diff --git a/internal/container/container_test.go b/internal/container/container_test.go new file mode 100644 index 000000000..5f166d144 --- /dev/null +++ b/internal/container/container_test.go @@ -0,0 +1,382 @@ +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//blobs/ + // [3] manifest: /v2//manifests/ + // + // 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] + + 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: ref.String(), + TLSVerify: common.BoolToPtr(false), + }, nil +} + +func (reg *Registry) Close() { + reg.server.Close() +}