debian-forge-composer/internal/container/container_test.go
2023-04-19 20:07:40 +02:00

385 lines
8.1 KiB
Go

package container_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"time"
"github.com/opencontainers/go-digest"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/container"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/manifest"
)
const rootLayer = `H4sIAAAJbogA/+SWUYqDMBCG53lP4V5g9x8dzRX2Bvtc0VIhEIhKe/wSKxgU6ktjC/O9hMzAQDL8
/8yltdb9DLeB0gEGKhHCg/UJsBAL54zKFBAC54ZzyrCUSMfYDydPgHfu6R/s5VePilOfzF/of/bv
vG2+lqhyFNGPddP53yjyegCBKcuNROZ77AmBoP+CmbIyqpEM5fqf+3/ubJtsCuz7P1b+L1Du/4f5
v+vrsVPu/Vq9P3ANk//d+x/MZv8TKNf/Qfqf9v9v5fLXK3/lKEc5ypm4AwAA//8DAE6E6nIAEgAA
`
// The following code implements a toy container registry to test with
// Blob interface
type Blob interface {
GetSize() int64
GetMediaType() string
GetDigest() digest.Digest
Reader() io.Reader
}
// dataBlob //
type dataBlob struct {
Data []byte
MediaType string
}
func NewDataBlobFromBase64(text string) dataBlob {
data, err := base64.StdEncoding.DecodeString(text)
if err != nil {
panic("decoding of text failed")
}
return dataBlob{
Data: data,
}
}
// Blob interface implementation
func (b dataBlob) GetSize() int64 {
return int64(len(b.Data))
}
func (b dataBlob) GetMediaType() string {
if b.MediaType != "" {
return b.MediaType
}
return manifest.DockerV2Schema2LayerMediaType
}
func (b dataBlob) GetDigest() digest.Digest {
return digest.FromBytes(b.Data)
}
func (b dataBlob) Reader() io.Reader {
return bytes.NewReader(b.Data)
}
func MakeDescriptorForBlob(b Blob) manifest.Schema2Descriptor {
return manifest.Schema2Descriptor{
MediaType: b.GetMediaType(),
Size: b.GetSize(),
Digest: b.GetDigest(),
}
}
// Repo //
type Repo struct {
blobs map[string]Blob
manifests map[string]*manifest.Schema2
images map[string]*manifest.Schema2List
tags map[string]string
}
func NewRepo() *Repo {
return &Repo{
blobs: make(map[string]Blob),
manifests: make(map[string]*manifest.Schema2),
tags: make(map[string]string),
images: make(map[string]*manifest.Schema2List),
}
}
func (r *Repo) AddBlob(b Blob) manifest.Schema2Descriptor {
desc := MakeDescriptorForBlob(b)
r.blobs[desc.Digest.String()] = b
return desc
}
func (r *Repo) AddObject(v interface{}, mediaType string) manifest.Schema2Descriptor {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
panic("could not marshal image object")
}
blob := dataBlob{
Data: data,
MediaType: mediaType,
}
return r.AddBlob(blob)
}
func (r *Repo) AddManifest(mf *manifest.Schema2) manifest.Schema2Descriptor {
desc := r.AddObject(mf, mf.MediaType)
r.manifests[desc.Digest.String()] = mf
return desc
}
func (r *Repo) AddImage(layers []Blob, arches []string, comment string, ctime time.Time) string {
blobs := make([]manifest.Schema2Descriptor, len(layers))
for i, layer := range layers {
blobs[i] = r.AddBlob(layer)
}
manifests := make([]manifest.Schema2ManifestDescriptor, len(arches))
for i, arch := range arches {
img := manifest.Schema2V1Image{
Architecture: arch,
OS: "linux",
Author: "osbuild",
Comment: comment,
Created: ctime,
}
// Add the config object
config := r.AddObject(img, manifest.DockerV2Schema2ConfigMediaType)
// make and add the manifest object
schema := manifest.Schema2FromComponents(config, blobs)
mf := r.AddManifest(schema)
desc := manifest.Schema2ManifestDescriptor{
Schema2Descriptor: mf,
Platform: manifest.Schema2PlatformSpec{
Architecture: arch,
OS: "linux",
},
}
manifests[i] = desc
}
list := manifest.Schema2ListFromComponents(manifests)
desc := r.AddObject(list, list.MediaType)
checksum := desc.Digest.String()
r.images[checksum] = list
r.tags["latest"] = checksum
return checksum
}
func (r *Repo) AddTag(checksum, tag string) {
if _, ok := r.images[checksum]; !ok {
panic("cannot tag: image not found: " + checksum)
}
r.tags[tag] = checksum
}
func WriteBlob(blob Blob, w http.ResponseWriter) {
w.Header().Add("Content-Type", blob.GetMediaType())
w.Header().Add("Content-Length", fmt.Sprintf("%d", blob.GetSize()))
w.Header().Add("Docker-Content-Digest", blob.GetDigest().String())
w.WriteHeader(http.StatusOK)
reader := blob.Reader()
_, err := io.Copy(w, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "error writing blob: %v", err)
}
}
func BlobIsManifest(blob Blob) bool {
mt := blob.GetMediaType()
return mt == manifest.DockerV2Schema2MediaType || mt == manifest.DockerV2ListMediaType
}
func (r *Repo) ServeManifest(ref string, w http.ResponseWriter, req *http.Request) {
if checksum, ok := r.tags[ref]; ok {
ref = checksum
}
blob, ok := r.blobs[ref]
if !ok || !BlobIsManifest(blob) {
fmt.Fprintf(os.Stderr, "manifest %s not found", ref)
http.NotFound(w, req)
return
}
WriteBlob(blob, w)
}
func (r *Repo) ServeBlob(ref string, w http.ResponseWriter, req *http.Request) {
blob, ok := r.blobs[ref]
if !ok {
fmt.Fprintf(os.Stderr, "blob %s not found", ref)
http.NotFound(w, req)
return
}
WriteBlob(blob, w)
}
// Registry //
type Registry struct {
server *httptest.Server
repos map[string]*Repo
}
func (reg *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
parts := strings.SplitN(req.URL.Path, "?", 1)
paths := strings.Split(strings.Trim(parts[0], "/"), "/")
// Possbile routes
// [1] version-check: /v2/
// [2] blobs: /v2/<repo_name>/blobs/<digest>
// [3] manifest: /v2/<repo_name>/manifests/<ref>
//
// we need at least 4 path components and path has to start with "/v2"
if len(paths) < 1 || paths[0] != "v2" {
http.NotFound(w, req)
return
}
// [1] version check
if len(paths) == 1 {
w.WriteHeader(200)
return
} else if len(paths) < 4 {
http.NotFound(w, req)
return
}
// we asserted that we have at least 4 path components
ref := paths[len(paths)-1]
cmd := paths[len(paths)-2]
repoName := strings.Join(paths[1:len(paths)-2], "/")
repo, ok := reg.repos[repoName]
if !ok {
fmt.Fprintf(os.Stderr, "repo %s not found", repoName)
http.NotFound(w, req)
return
}
if cmd == "manifests" {
repo.ServeManifest(ref, w, req)
} else if cmd == "blobs" {
repo.ServeBlob(ref, w, req)
} else {
http.NotFound(w, req)
}
}
func NewTestRegistry() *Registry {
reg := &Registry{
repos: make(map[string]*Repo),
}
reg.server = httptest.NewTLSServer(reg)
return reg
}
func (reg *Registry) AddRepo(name string) *Repo {
repo := NewRepo()
reg.repos[name] = repo
return repo
}
func (reg *Registry) GetRef(repo string) string {
return fmt.Sprintf("%s/%s", reg.server.Listener.Addr().String(), repo)
}
func (reg *Registry) Resolve(target, arch string) (container.Spec, error) {
ref, err := reference.ParseNormalizedNamed(target)
if err != nil {
return container.Spec{}, fmt.Errorf("failed to parse '%s': %w", target, err)
}
domain := reference.Domain(ref)
tag := "latest"
var checksum string
if tagged, ok := ref.(reference.NamedTagged); ok {
tag = tagged.Tag()
}
if digested, ok := ref.(reference.Digested); ok {
checksum = string(digested.Digest())
}
if domain != reg.server.Listener.Addr().String() {
return container.Spec{}, fmt.Errorf("unknown domain")
}
ref = reference.TrimNamed(ref)
path := reference.Path(ref)
repo, ok := reg.repos[path]
if !ok {
return container.Spec{}, fmt.Errorf("unknown repo")
}
if checksum == "" {
checksum, ok = repo.tags[tag]
if !ok {
return container.Spec{}, fmt.Errorf("unknown tag")
}
}
lst, ok := repo.images[checksum]
listDigest := checksum
if ok {
checksum = ""
for _, m := range lst.Manifests {
if m.Platform.Architecture == arch {
checksum = m.Digest.String()
break
}
}
if checksum == "" {
return container.Spec{}, fmt.Errorf("unsupported architecture")
}
}
mf, ok := repo.manifests[checksum]
if !ok {
return container.Spec{}, fmt.Errorf("unknown digest")
}
return container.Spec{
Source: ref.String(),
Digest: checksum,
ImageID: mf.ConfigDescriptor.Digest.String(),
LocalName: target,
TLSVerify: common.ToPtr(false),
ListDigest: listDigest,
}, nil
}
func (reg *Registry) Close() {
reg.server.Close()
}