nixery/manifest/manifest.go
Markus Rudy eb845efba4 fix(nixery): strictly adhere to OCI image spec
nixery.dev uses the vnd.docker.container.image.v1 format, which is
recognized by the OCI [1] and originally defined by Docker [2]. The
config field in this image format, which this commit is about, is
even portable between the Docker and OCI formats (the Docker Golang
library embeds the OCI definition [3]).

The attribute names in what's called ImageConfig in [3] are specified as
PascalCase, which effectively means that the names Env and Cmd used by
nixery need to be capitalized. The lowercase variant is not causing a
lot of issues because most container tooling is written in Golang, which
allows case-insensitive matches when deserializing JSON. Languages that
parse strictly either miss the configuration values, or fail due to
unknown attributes. This commit capitalizes Cmd and Env to accomodate
strict parsers.

[1]: https://github.com/opencontainers/image-spec/blob/365fa41/media-types.md?plain=1#L70
[2]: https://github.com/moby/moby/blob/v20.10.8/image/spec/v1.2.md#image-json-description
[3]: https://github.com/opencontainers/image-spec/blob/365fa41/specs-go/v1/config.go#L24

Change-Id: Ibee597a64d36c008dea83a3b7a0d8e59b8287d0d
Signed-off-by: Markus Rudy <webmaster@burgerdev.de>
Co-authored-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11012
Autosubmit: lukegb <lukegb@tvl.fyi>
Reviewed-by: lukegb <lukegb@tvl.fyi>
Tested-by: BuildkiteCI
2024-02-22 16:56:02 +00:00

136 lines
3.4 KiB
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
}