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 `<name>@<digest>` 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.
This commit is contained in:
Christian Kellner 2022-07-20 21:18:30 +02:00 committed by Ondřej Budai
parent bd42243882
commit 60607af26c
4 changed files with 233 additions and 0 deletions

1
go.mod
View file

@ -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

View file

@ -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
}

View file

@ -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,
}
}

1
vendor/modules.txt vendored
View file

@ -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