debian-forge-composer/vendor/github.com/osbuild/images/pkg/container/client.go
Achilleas Koutsou c77ca66191 go.mod: update osbuild/images to v0.148.0
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

---
2025-06-05 10:35:58 +02:00

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, &copy.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
}