From 60607af26c72ff7fd3a6d346fe0e5e2ca31ece30 Mon Sep 17 00:00:00 2001 From: Christian Kellner Date: Wed, 20 Jul 2022 21:18:30 +0200 Subject: [PATCH] container: ability to resolve containers to specs Add a new `Resolve` method to `Client` that will resolve its `Target` to the corresponding manifest digest id and its corresponding iamge identifier. The former can be used in the URL to fetch a specific image from the registry via `@` and the latter uniquely identifies a container image via the hash of its configuration object. This should stay the same across pulls and is also the id returned via `podman pull` and `podman images`. Since (most) container images are OS and architecture specific a tag often points to a manifest list that contains all available options. Therefore the resolve operation needs to choose the correct arch for image. A new pair of getters `Set{Architecture,Variant}Choice` lets the user control which architecture/variant is selected during the resolution process. --- go.mod | 1 + internal/container/client.go | 198 +++++++++++++++++++++++++++++++++++ internal/container/spec.go | 33 ++++++ vendor/modules.txt | 1 + 4 files changed, 233 insertions(+) create mode 100644 internal/container/spec.go diff --git a/go.mod b/go.mod index f44dae7fe..177ff3bd1 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/labstack/echo/v4 v4.7.2 github.com/labstack/gommon v0.3.1 github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 github.com/openshift-online/ocm-sdk-go v0.1.266 github.com/oracle/oci-go-sdk/v54 v54.0.0 github.com/prometheus/client_golang v1.12.1 diff --git a/internal/container/client.go b/internal/container/client.go index 9110b781b..68d942605 100644 --- a/internal/container/client.go +++ b/internal/container/client.go @@ -24,6 +24,8 @@ import ( "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" + + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) const ( @@ -127,6 +129,8 @@ func NewClient(target string) (*Client, error) { SystemRegistriesConfPath: "", BigFilesTemporaryDir: "/var/tmp", + OSChoice: "linux", + AuthFilePath: defaultAuthFile, }, policy: policy, @@ -140,6 +144,39 @@ func (cl *Client) SetAuthFilePath(path string) { cl.sysCtx.AuthFilePath = path } +func (cl *Client) SetArchitectureChoice(arch string) { + // Translate some well-known Composer architecture strings + // into the corresponding container ones + + variant := "" + + switch arch { + case "x86_64": + arch = "amd64" + + case "aarch64": + arch = "arm64" + if variant == "" { + variant = "v8" + } + + case "armhfp": + arch = "arm" + if variant == "" { + variant = "v7" + } + + //ppc64le and s390x are the same + } + + cl.sysCtx.ArchitectureChoice = arch + cl.sysCtx.VariantChoice = variant +} + +func (cl *Client) SetVariantChoice(variant string) { + cl.sysCtx.VariantChoice = variant +} + // SetCredentials will set username and password for Client func (cl *Client) SetCredentials(username, password string) { @@ -265,3 +302,164 @@ func (cl *Client) UploadImage(ctx context.Context, from, tag string) (digest.Dig return manifestDigest, nil } + +// A RawManifest contains the raw manifest Data and its MimeType +type RawManifest struct { + Data []byte + MimeType string +} + +// Digest computes the digest from the raw manifest data +func (m RawManifest) Digest() (digest.Digest, error) { + return manifest.Digest(m.Data) +} + +// GetManifest fetches the raw manifest data from the server. If digest is not empty +// it will override any given tag for the Client's Target. +func (cl *Client) GetManifest(ctx context.Context, digest digest.Digest) (r RawManifest, err error) { + target := cl.Target + + if digest != "" { + t := reference.TrimNamed(cl.Target) + t, err = reference.WithDigest(t, digest) + if err != nil { + return + } + + target = t + } + + ref, err := docker.NewReference(target) + if err != nil { + return + } + + src, err := ref.NewImageSource(ctx, cl.sysCtx) + + if err != nil { + return + } + + defer func() { + if e := src.Close(); e != nil { + err = fmt.Errorf("could not close image: %w", e) + } + }() + + retryOpts := retry.RetryOptions{ + MaxRetry: cl.MaxRetries, + } + + if err = retry.RetryIfNecessary(ctx, func() error { + r.Data, r.MimeType, err = src.GetManifest(ctx, nil) + return err + }, &retryOpts); err != nil { + return + } + + return +} + +type manifestList interface { + ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) +} + +type resolvedIds struct { + Manifest digest.Digest + Config digest.Digest +} + +func (cl *Client) resolveManifestList(ctx context.Context, list manifestList) (resolvedIds, error) { + digest, err := list.ChooseInstance(cl.sysCtx) + if err != nil { + return resolvedIds{}, err + } + + raw, err := cl.GetManifest(ctx, digest) + + if err != nil { + return resolvedIds{}, fmt.Errorf("error getting manifest: %w", err) + } + + if err != nil { + return resolvedIds{}, nil + } + + return cl.resolveRawManifest(ctx, raw) +} + +func (cl *Client) resolveRawManifest(ctx context.Context, rm RawManifest) (resolvedIds, error) { + + var imageID digest.Digest + + switch rm.MimeType { + case manifest.DockerV2ListMediaType: + list, err := manifest.Schema2ListFromManifest(rm.Data) + if err != nil { + return resolvedIds{}, err + } + + return cl.resolveManifestList(ctx, list) + + case imgspecv1.MediaTypeImageIndex: + index, err := manifest.OCI1IndexFromManifest(rm.Data) + if err != nil { + return resolvedIds{}, err + } + + return cl.resolveManifestList(ctx, index) + + case imgspecv1.MediaTypeImageManifest: + m, err := manifest.OCI1FromManifest(rm.Data) + if err != nil { + return resolvedIds{}, nil + } + imageID = m.ConfigInfo().Digest + + case manifest.DockerV2Schema2MediaType: + m, err := manifest.Schema2FromManifest(rm.Data) + + if err != nil { + return resolvedIds{}, nil + } + + imageID = m.ConfigInfo().Digest + + default: + return resolvedIds{}, fmt.Errorf("unsupported manifest format '%s'", rm.MimeType) + } + + dg, err := rm.Digest() + + if err != nil { + return resolvedIds{}, err + } + + return resolvedIds{ + Manifest: dg, + Config: imageID, + }, nil +} + +// Resolve the Client's Target to the manifest digest and the corresponding image id +// which is the digest of the configuration object. It uses the architecture and +// variant specified via SetArchitectureChoice or the corresponding defaults for +// the host. +func (cl *Client) Resolve(ctx context.Context, name string) (Spec, error) { + + raw, err := cl.GetManifest(ctx, "") + + if err != nil { + return Spec{}, fmt.Errorf("error getting manifest: %w", err) + } + + ids, err := cl.resolveRawManifest(ctx, raw) + if err != nil { + return Spec{}, err + } + + spec := NewSpec(cl.Target, ids.Manifest, ids.Config) + spec.TLSVerify = cl.GetTLSVerify() + + return spec, nil +} diff --git a/internal/container/spec.go b/internal/container/spec.go new file mode 100644 index 000000000..ffd781c8d --- /dev/null +++ b/internal/container/spec.go @@ -0,0 +1,33 @@ +package container + +import ( + "github.com/containers/image/v5/docker/reference" + "github.com/opencontainers/go-digest" +) + +// A Spec is the specification of how to get a specific +// container from a Source and under what LocalName to +// store it in an image. The container is identified by +// at the Source via Digest and ImageID. The latter one +// should remain the same in the target image as well. +type Spec struct { + Source string // does not include the manifest digest + Digest string // digest of the manifest at the Source + TLSVerify *bool // controls TLS verification + ImageID string // container image identifier + LocalName string // name to use inside the image +} + +// NewSpec creates a new Spec from the essential information. +// It also converts is the transition point from container +// specific types (digest.Digest) to generic types (string). +func NewSpec(source reference.Named, digest, imageID digest.Digest) Spec { + name := source.Name() + return Spec{ + Source: name, + Digest: digest.String(), + ImageID: imageID.String(), + + LocalName: name, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a5c9e91d9..7c04fa05f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -436,6 +436,7 @@ github.com/modern-go/reflect2 ## explicit github.com/opencontainers/go-digest # github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 +## explicit github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 # github.com/opencontainers/runc v1.1.1