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:
parent
bd42243882
commit
60607af26c
4 changed files with 233 additions and 0 deletions
1
go.mod
1
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
33
internal/container/spec.go
Normal file
33
internal/container/spec.go
Normal 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
1
vendor/modules.txt
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue