tag v0.145.0 Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com> Changes with 0.145.0 ---------------- * github: run dependabot gomod action weekly (osbuild/images#1476) * Author: Achilleas Koutsou, Reviewers: Lukáš Zapletal — Somewhere on the Internet, 2025-05-12 --- tag v0.146.0 Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com> Changes with 0.146.0 ---------------- * Fixes for ESP partition: Make optional, set label (osbuild/images#1525) * Author: Alexander Larsson, Reviewers: Achilleas Koutsou, Brian C. Lane * Initial automotive work: custom selinux policy, separate build container for bootc, and ext4 verity (osbuild/images#1519) * Author: Alexander Larsson, Reviewers: Achilleas Koutsou, Simon de Vlieger * Update snapshots to 20250512 (osbuild/images#1515) * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger * disk: make auto-generated /boot 1 GiB big (osbuild/images#1499) * Author: Ondřej Budai, Reviewers: Achilleas Koutsou, Michael Vogt * distro.yaml: Clean up yamllint errors and warnings (osbuild/images#1523) * Author: Brian C. Lane, Reviewers: Michael Vogt, Simon de Vlieger * distro/rhel9: make /boot 1 GiB everywhere (osbuild/images#1498) * Author: Ondřej Budai, Reviewers: Michael Vogt, Simon de Vlieger * distro: move disk/container image types into pure YAML (COMPOSER-2533) (osbuild/images#1508) * Author: Michael Vogt, Reviewers: Achilleas Koutsou, Simon de Vlieger * fedora: move all image types into pure YAML (osbuild/images#1514) * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger * fsnode: fix go-1.24 errors (osbuild/images#1521) * Author: Michael Vogt, Reviewers: Ondřej Budai, Tomáš Hozza * osbuild: add JSON/YAML unmarshal to UdevRulesStageOptions (osbuild/images#1489) * Author: Michael Vogt, Reviewers: Achilleas Koutsou, Simon de Vlieger * test: Run more distro tests in parallel (osbuild/images#1483) * Author: Brian C. Lane, Reviewers: Michael Vogt, Simon de Vlieger — Somewhere on the Internet, 2025-05-19 --- tag v0.147.0 Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com> Changes with 0.147.0 ---------------- * Add support for setting partition uuid and label (osbuild/images#1543) * Author: Alexander Larsson, Reviewers: Achilleas Koutsou, Simon de Vlieger * Cleanup of new APIs (mkfs options and build container) (osbuild/images#1526) * Author: Alexander Larsson, Reviewers: Achilleas Koutsou, Simon de Vlieger * distro/rhel: remove the user/group warnings for edge-commits (osbuild/images#1538) * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Simon de Vlieger — Somewhere on the Internet, 2025-05-20 --- tag v0.148.0 Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com> Changes with 0.148.0 ---------------- * Makefile: add vet command to check for consistent struct tags (osbuild/images#1554) * Author: Michael Vogt, Reviewers: Lukáš Zapletal, Simon de Vlieger * disk: tiny tweaks for the new MkfsOptions support (osbuild/images#1545) * Author: Michael Vogt, Reviewers: Achilleas Koutsou, Alexander Larsson, Lukáš Zapletal * fedora/many: increase `/boot` to 1 GiB (HMS-8604) (osbuild/images#1557) * Author: Simon de Vlieger, Reviewers: Achilleas Koutsou, Ondřej Budai * fedora/wsl: include `wsl-setup` (HMS-8573) (osbuild/images#1550) * Author: Simon de Vlieger, Reviewers: Brian C. Lane, Michael Vogt * fedora: add `anaconda.ModuleUsers` to ImageInstallerImage (osbuild/images#1558) * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger * fedora: implement setting of the RootfsType via YAML (osbuild/images#1544) * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger * rhel10: move ImageConfig into pure YAML (osbuild/images#1542) * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger — Somewhere on the Internet, 2025-05-26 ---
592 lines
16 KiB
Go
592 lines
16 KiB
Go
// package container implements a client for a container
|
|
// registry. It can be used to upload container images.
|
|
package container
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
_ "github.com/containers/image/v5/docker/archive"
|
|
_ "github.com/containers/image/v5/oci/archive"
|
|
_ "github.com/containers/image/v5/oci/layout"
|
|
"github.com/containers/storage"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/containers/common/pkg/retry"
|
|
"github.com/containers/image/v5/copy"
|
|
"github.com/containers/image/v5/docker"
|
|
"github.com/containers/image/v5/docker/reference"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/signature"
|
|
"github.com/containers/image/v5/transports"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/opencontainers/go-digest"
|
|
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
"github.com/osbuild/images/internal/common"
|
|
"github.com/osbuild/images/pkg/arch"
|
|
)
|
|
|
|
const (
|
|
DefaultUserAgent = "osbuild-composer/1.0"
|
|
DefaultPolicyPath = "/etc/containers/policy.json"
|
|
)
|
|
|
|
// GetDefaultAuthFile returns the authentication file to use for the
|
|
// current environment.
|
|
//
|
|
// This is basically a re-implementation of `getPathToAuthWithOS` from
|
|
// containers/image/pkg/docker/config/config.go[1], but we ensure that
|
|
// the returned path is either accessible. This is needed since any
|
|
// other error than os.ErrNotExist will lead to an overall failure and
|
|
// thus prevent any operation even with public resources.
|
|
//
|
|
// [1] https://github.com/containers/image/blob/55ea76c7db702ed1af60924a0b57c8da533d9e5a/pkg/docker/config/config.go#L506
|
|
func GetDefaultAuthFile() string {
|
|
|
|
checkAccess := func(path string) bool {
|
|
err := unix.Access(path, unix.R_OK)
|
|
return err == nil
|
|
}
|
|
|
|
if authFile := os.Getenv("REGISTRY_AUTH_FILE"); authFile != "" {
|
|
if checkAccess(authFile) {
|
|
return authFile
|
|
}
|
|
}
|
|
|
|
if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" {
|
|
if checkAccess(runtimeDir) {
|
|
return filepath.Join(runtimeDir, "containers", "auth.json")
|
|
}
|
|
}
|
|
|
|
if rundir := filepath.FromSlash("/run/containers"); checkAccess(rundir) {
|
|
return filepath.Join(rundir, strconv.Itoa(os.Getuid()), "auth.json")
|
|
}
|
|
|
|
return filepath.FromSlash("/var/empty/containers-auth.json")
|
|
}
|
|
|
|
// ApplyDefaultPath checks if the target includes a domain and if it doesn't adds the default ones
|
|
// to the returned string. If also returns a bool indicating whether the defaults were applied
|
|
func ApplyDefaultDomainPath(target, defaultDomain, defaultPath string) (string, bool) {
|
|
appliedDefaults := false
|
|
i := strings.IndexRune(target, '/')
|
|
if i == -1 || (!strings.ContainsAny(target[:i], ".:") && target[:i] != "localhost") {
|
|
if defaultDomain != "" {
|
|
base := defaultDomain
|
|
if defaultPath != "" {
|
|
base = fmt.Sprintf("%s/%s", base, defaultPath)
|
|
}
|
|
target = fmt.Sprintf("%s/%s", base, target)
|
|
appliedDefaults = true
|
|
}
|
|
}
|
|
|
|
return target, appliedDefaults
|
|
}
|
|
|
|
// A Client to interact with the given Target object at a
|
|
// container registry, like e.g. uploading an image to.
|
|
// All mentioned defaults are only set when using the
|
|
// NewClient constructor.
|
|
type Client struct {
|
|
Target reference.Named // the target object to interact with
|
|
|
|
ReportWriter io.Writer // used for writing status reports, defaults to os.Stdout
|
|
|
|
PrecomputeDigests bool // precompute digest in order to avoid uploads
|
|
MaxRetries int // how often to retry http requests
|
|
|
|
UserAgent string // user agent string to use for requests, defaults to DefaultUserAgent
|
|
|
|
// internal state
|
|
policy *signature.Policy
|
|
sysCtx *types.SystemContext
|
|
|
|
store string // another store location other than the main one, useful for testing
|
|
}
|
|
|
|
// NewClient constructs a new Client for target with default options.
|
|
// It will add the "latest" tag if target does not contain it.
|
|
func NewClient(target string) (*Client, error) {
|
|
|
|
ref, err := reference.ParseNormalizedNamed(target)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse '%s': %w", target, err)
|
|
}
|
|
|
|
var policy *signature.Policy
|
|
if _, err := os.Stat(DefaultPolicyPath); err == nil {
|
|
policy, err = signature.NewPolicyFromFile(DefaultPolicyPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
policy = &signature.Policy{
|
|
Default: []signature.PolicyRequirement{
|
|
signature.NewPRInsecureAcceptAnything(),
|
|
},
|
|
}
|
|
}
|
|
|
|
client := Client{
|
|
Target: reference.TagNameOnly(ref),
|
|
|
|
ReportWriter: os.Stdout,
|
|
PrecomputeDigests: true,
|
|
|
|
UserAgent: DefaultUserAgent,
|
|
|
|
sysCtx: &types.SystemContext{
|
|
RegistriesDirPath: "",
|
|
SystemRegistriesConfPath: "",
|
|
BigFilesTemporaryDir: "/var/tmp",
|
|
|
|
OSChoice: "linux",
|
|
|
|
AuthFilePath: GetDefaultAuthFile(),
|
|
},
|
|
policy: policy,
|
|
store: "/var/lib/containers/storage",
|
|
}
|
|
|
|
return &client, nil
|
|
}
|
|
|
|
// SetAuthFilePath sets the location of the `containers-auth.json(5)` file.
|
|
func (cl *Client) SetAuthFilePath(path string) {
|
|
cl.sysCtx.AuthFilePath = path
|
|
}
|
|
|
|
// GetAuthFilePath gets the location of the `containers-auth.json(5)` file.
|
|
func (cl *Client) GetAuthFilePath() string {
|
|
return cl.sysCtx.AuthFilePath
|
|
}
|
|
|
|
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) {
|
|
|
|
if cl.sysCtx.DockerAuthConfig == nil {
|
|
cl.sysCtx.DockerAuthConfig = &types.DockerAuthConfig{}
|
|
}
|
|
|
|
cl.sysCtx.DockerAuthConfig.Username = username
|
|
cl.sysCtx.DockerAuthConfig.Password = password
|
|
}
|
|
|
|
func (cl *Client) SetDockerCertPath(path string) {
|
|
cl.sysCtx.DockerCertPath = path
|
|
}
|
|
|
|
// SetSkipTLSVerify controls if TLS verification happens when
|
|
// making requests. If nil is passed it falls back to the default.
|
|
func (cl *Client) SetTLSVerify(verify *bool) {
|
|
if verify == nil {
|
|
cl.sysCtx.DockerInsecureSkipTLSVerify = types.OptionalBoolUndefined
|
|
} else {
|
|
cl.sysCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!*verify)
|
|
}
|
|
}
|
|
|
|
// GetSkipTLSVerify returns current TLS verification state.
|
|
func (cl *Client) GetTLSVerify() *bool {
|
|
|
|
skip := cl.sysCtx.DockerInsecureSkipTLSVerify
|
|
|
|
if skip == types.OptionalBoolUndefined {
|
|
return nil
|
|
}
|
|
|
|
// NB: we invert the state, i.e. verify == (skip == false)
|
|
return common.ToPtr(skip == types.OptionalBoolFalse)
|
|
}
|
|
|
|
// SkipTLSVerify is a convenience helper that internally calls
|
|
// SetTLSVerify with false
|
|
func (cl *Client) SkipTLSVerify() {
|
|
cl.SetTLSVerify(common.ToPtr(false))
|
|
}
|
|
|
|
func parseImageName(name string) (types.ImageReference, error) {
|
|
|
|
parts := strings.SplitN(name, ":", 2)
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid image name '%s'", name)
|
|
}
|
|
|
|
transport := transports.Get(parts[0])
|
|
if transport == nil {
|
|
return nil, fmt.Errorf("unknown transport '%s'", parts[0])
|
|
}
|
|
|
|
return transport.ParseReference(parts[1])
|
|
}
|
|
|
|
// UploadImage takes an container image located at from and uploads it
|
|
// to the Target of Client. If tag is set, i.e. not the empty string,
|
|
// it will replace any previously set tag or digest of the target.
|
|
// Returns the digest of the manifest that was written to the server.
|
|
func (cl *Client) UploadImage(ctx context.Context, from, tag string) (digest.Digest, error) {
|
|
|
|
targetCtx := *cl.sysCtx
|
|
targetCtx.DockerRegistryPushPrecomputeDigests = cl.PrecomputeDigests
|
|
|
|
policyContext, err := signature.NewPolicyContext(cl.policy)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
srcRef, err := parseImageName(from)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid source name '%s': %w", from, err)
|
|
}
|
|
|
|
target := cl.Target
|
|
|
|
if tag != "" {
|
|
target = reference.TrimNamed(target)
|
|
target, err = reference.WithTag(target, tag)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error creating reference with tag '%s': %w", tag, err)
|
|
}
|
|
}
|
|
|
|
destRef, err := docker.NewReference(target)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
retryOpts := retry.RetryOptions{
|
|
MaxRetry: cl.MaxRetries,
|
|
}
|
|
|
|
var manifestDigest digest.Digest
|
|
|
|
err = retry.RetryIfNecessary(ctx, func() error {
|
|
manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{
|
|
RemoveSignatures: false,
|
|
SignBy: "",
|
|
SignPassphrase: "",
|
|
ReportWriter: cl.ReportWriter,
|
|
SourceCtx: cl.sysCtx,
|
|
DestinationCtx: &targetCtx,
|
|
ForceManifestMIMEType: "",
|
|
ImageListSelection: copy.CopyAllImages,
|
|
PreserveDigests: false,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
manifestDigest, err = manifest.Digest(manifestBytes)
|
|
|
|
return err
|
|
|
|
}, &retryOpts)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return manifestDigest, nil
|
|
}
|
|
|
|
// A RawManifest contains the raw manifest Data and its MimeType
|
|
type RawManifest struct {
|
|
Data []byte
|
|
MimeType string
|
|
Arch arch.Arch
|
|
}
|
|
|
|
// Digest computes the digest from the raw manifest data
|
|
func (m RawManifest) Digest() (digest.Digest, error) {
|
|
return manifest.Digest(m.Data)
|
|
}
|
|
|
|
func (cl *Client) getImageRef(id string, local bool) (types.ImageReference, error) {
|
|
if local {
|
|
imageName := cl.Target.String()
|
|
if id != "" {
|
|
imageName = id
|
|
}
|
|
options := fmt.Sprintf("containers-storage:[overlay@%s+/run/containers/storage]%s", cl.store, imageName)
|
|
return alltransports.ParseImageName(options)
|
|
}
|
|
|
|
return docker.NewReference(cl.Target)
|
|
}
|
|
|
|
func (cl *Client) resolveContainerImageArch(ctx context.Context, ref types.ImageReference) (*arch.Arch, error) {
|
|
img, err := ref.NewImage(ctx, cl.sysCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer img.Close()
|
|
info, err := img.Inspect(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a, err := arch.FromString(info.Architecture)
|
|
return &a, err
|
|
}
|
|
|
|
func (cl *Client) getLocalImageIDFromDigest(instance digest.Digest) (string, error) {
|
|
store, err := storage.GetStore(storage.StoreOptions{GraphRoot: cl.store})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
images, err := store.ImagesByDigest(instance)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(images) == 0 {
|
|
return "", fmt.Errorf("Unable to find image id for digest: %v", instance)
|
|
}
|
|
// the `ImagesByDigest` function always returns a list
|
|
// of images. The list could be larger than 1 in the case
|
|
// since it searches all stores for the matching digest,
|
|
// so it is okay to return the first item.
|
|
return images[0].ID, nil
|
|
}
|
|
|
|
// 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, instanceDigest digest.Digest, local bool) (r RawManifest, err error) {
|
|
var localId string
|
|
var overrideDigest *digest.Digest
|
|
|
|
if instanceDigest != "" {
|
|
if local {
|
|
localId, err = cl.getLocalImageIDFromDigest(instanceDigest)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
} else {
|
|
// We can pass the instance digest, if it is nil, then this is the primary manifest.
|
|
// If it is not nil, then this is the instance digest and the primary manifest is a
|
|
// manifest list. The `GetManifest` call will then retrieve the manifest for the
|
|
// desired instance, see:
|
|
// https://github.com/containers/image/blob/cdb2f596a95018444f4dee0993e321d3d8bc328d/types/types.go#L252C2-L253C121
|
|
overrideDigest = &instanceDigest
|
|
}
|
|
}
|
|
|
|
ref, err := cl.getImageRef(localId, local)
|
|
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, overrideDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// getting the container image arch doesn't work with local manifest lists
|
|
if local && (r.MimeType == imgspecv1.MediaTypeImageIndex || r.MimeType == manifest.DockerV2ListMediaType) {
|
|
return nil
|
|
}
|
|
|
|
imageArch, err := cl.resolveContainerImageArch(ctx, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Arch = *imageArch
|
|
|
|
return nil
|
|
}, &retryOpts); err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type manifestList interface {
|
|
ChooseInstance(ctx *types.SystemContext) (digest.Digest, error)
|
|
}
|
|
|
|
type resolvedIds struct {
|
|
Manifest digest.Digest
|
|
Config digest.Digest
|
|
ListManifest digest.Digest
|
|
}
|
|
|
|
func (cl *Client) resolveManifestList(ctx context.Context, list manifestList, local bool) (resolvedIds, *arch.Arch, error) {
|
|
digest, err := list.ChooseInstance(cl.sysCtx)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
|
|
raw, err := cl.GetManifest(ctx, digest, local)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, fmt.Errorf("error getting manifest: %w", err)
|
|
}
|
|
|
|
ids, _, err := cl.resolveRawManifest(ctx, raw, local)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
|
|
return ids, &raw.Arch, err
|
|
}
|
|
|
|
func (cl *Client) resolveRawManifest(ctx context.Context, rm RawManifest, local bool) (resolvedIds, *arch.Arch, error) {
|
|
|
|
var imageID digest.Digest
|
|
|
|
switch rm.MimeType {
|
|
case manifest.DockerV2ListMediaType:
|
|
list, err := manifest.Schema2ListFromManifest(rm.Data)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
|
|
// Save digest of the manifest list as well.
|
|
ids, imageArch, err := cl.resolveManifestList(ctx, list, local)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
// NOTE: Comment in Digest() source says this should never fail. Ignore the error.
|
|
ids.ListManifest, _ = rm.Digest()
|
|
return ids, imageArch, nil
|
|
|
|
case imgspecv1.MediaTypeImageIndex:
|
|
index, err := manifest.OCI1IndexFromManifest(rm.Data)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
|
|
// Save digest of the manifest list as well.
|
|
ids, imageArch, err := cl.resolveManifestList(ctx, index, local)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
// NOTE: Comment in Digest() source says this should never fail. Ignore the error.
|
|
ids.ListManifest, _ = rm.Digest()
|
|
return ids, imageArch, nil
|
|
|
|
case imgspecv1.MediaTypeImageManifest:
|
|
m, err := manifest.OCI1FromManifest(rm.Data)
|
|
if err != nil {
|
|
return resolvedIds{}, nil, nil
|
|
}
|
|
imageID = m.ConfigInfo().Digest
|
|
|
|
case manifest.DockerV2Schema2MediaType:
|
|
m, err := manifest.Schema2FromManifest(rm.Data)
|
|
|
|
if err != nil {
|
|
return resolvedIds{}, nil, nil
|
|
}
|
|
|
|
imageID = m.ConfigInfo().Digest
|
|
|
|
default:
|
|
return resolvedIds{}, nil, fmt.Errorf("unsupported manifest format '%s'", rm.MimeType)
|
|
}
|
|
|
|
dg, err := rm.Digest()
|
|
|
|
if err != nil {
|
|
return resolvedIds{}, nil, err
|
|
}
|
|
|
|
return resolvedIds{
|
|
Manifest: dg,
|
|
Config: imageID,
|
|
}, nil, 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, local bool) (Spec, error) {
|
|
|
|
raw, err := cl.GetManifest(ctx, "", local)
|
|
if err != nil {
|
|
return Spec{}, fmt.Errorf("error getting manifest: %w", err)
|
|
}
|
|
|
|
ids, imageArch, err := cl.resolveRawManifest(ctx, raw, local)
|
|
if err != nil {
|
|
return Spec{}, err
|
|
}
|
|
|
|
spec := NewSpec(
|
|
cl.Target,
|
|
ids.Manifest,
|
|
ids.Config,
|
|
cl.GetTLSVerify(),
|
|
ids.ListManifest.String(),
|
|
name,
|
|
local,
|
|
)
|
|
|
|
if imageArch != nil {
|
|
spec.Arch = *imageArch
|
|
} else {
|
|
spec.Arch = raw.Arch
|
|
}
|
|
|
|
return spec, nil
|
|
}
|