about summary refs log tree commit diff
path: root/tools/nixery/manifest/manifest.go
// Copyright 2022 The TVL Contributors
// SPDX-License-Identifier: Apache-2.0

// Package image implements logic for creating the image metadata
// (such as the image manifest and configuration).
package manifest

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"sort"
)

const (
	// manifest constants
	schemaVersion = 2

	// media types
	ManifestType = "application/vnd.docker.distribution.manifest.v2+json"
	LayerType    = "application/vnd.docker.image.rootfs.diff.tar.gzip"
	configType   = "application/vnd.docker.container.image.v1+json"

	// image config constants
	os     = "linux"
	fsType = "layers"
)

type Entry struct {
	MediaType string `json:"mediaType,omitempty"`
	Size      int64  `json:"size"`
	Digest    string `json:"digest"`

	// These fields are internal to Nixery and not part of the
	// serialised entry.
	MergeRating uint64 `json:"-"`
	TarHash     string `json:",omitempty"`
}

type manifest struct {
	SchemaVersion int     `json:"schemaVersion"`
	MediaType     string  `json:"mediaType"`
	Config        Entry   `json:"config"`
	Layers        []Entry `json:"layers"`
}

type imageConfig struct {
	Architecture string `json:"architecture"`
	OS           string `json:"os"`

	RootFS struct {
		FSType  string   `json:"type"`
		DiffIDs []string `json:"diff_ids"`
	} `json:"rootfs"`

	Config struct {
		Cmd []string `json:",omitempty"`
		Env []string `json:",omitempty"`
	} `json:"config"`
}

// ConfigLayer represents the configuration layer to be included in
// the manifest, containing its JSON-serialised content and SHA256
// hash.
type ConfigLayer struct {
	Config []byte
	SHA256 string
}

// imageConfig creates an image configuration with the values set to
// the constant defaults.
//
// Outside of this module the image configuration is treated as an
// opaque blob and it is thus returned as an already serialised byte
// array and its SHA256-hash.
func configLayer(arch string, hashes []string, cmd string) ConfigLayer {
	c := imageConfig{}
	c.Architecture = arch
	c.OS = os
	c.RootFS.FSType = fsType
	c.RootFS.DiffIDs = hashes
	if cmd != "" {
		c.Config.Cmd = []string{cmd}
	}
	c.Config.Env = []string{"SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"}

	j, _ := json.Marshal(c)

	return ConfigLayer{
		Config: j,
		SHA256: fmt.Sprintf("%x", sha256.Sum256(j)),
	}
}

// Manifest creates an image manifest from the specified layer entries
// and returns its JSON-serialised form as well as the configuration
// layer.
//
// Callers do not need to set the media type for the layer entries.
func Manifest(arch string, layers []Entry, cmd string) (json.RawMessage, ConfigLayer) {
	// Sort layers by their merge rating, from highest to lowest.
	// This makes it likely for a contiguous chain of shared image
	// layers to appear at the beginning of a layer.
	//
	// Due to moby/moby#38446 Docker considers the order of layers
	// when deciding which layers to download again.
	sort.Slice(layers, func(i, j int) bool {
		return layers[i].MergeRating > layers[j].MergeRating
	})

	hashes := make([]string, len(layers))
	for i, l := range layers {
		hashes[i] = l.TarHash
		l.MediaType = LayerType
		l.TarHash = ""
		layers[i] = l
	}

	c := configLayer(arch, hashes, cmd)

	m := manifest{
		SchemaVersion: schemaVersion,
		MediaType:     ManifestType,
		Config: Entry{
			MediaType: configType,
			Size:      int64(len(c.Config)),
			Digest:    "sha256:" + c.SHA256,
		},
		Layers: layers,
	}

	j, _ := json.Marshal(m)

	return json.RawMessage(j), c
}