crossplane
215 строк · 6.9 Кб
1/*
2Copyright 2023 The Crossplane Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package xpkg18
19import (20"context"21"fmt"22"os"23"path/filepath"24
25"github.com/google/go-containerregistry/pkg/authn"26"github.com/google/go-containerregistry/pkg/name"27v1 "github.com/google/go-containerregistry/pkg/v1"28"github.com/google/go-containerregistry/pkg/v1/empty"29"github.com/google/go-containerregistry/pkg/v1/mutate"30"github.com/google/go-containerregistry/pkg/v1/remote"31"github.com/google/go-containerregistry/pkg/v1/tarball"32"github.com/spf13/afero"33"golang.org/x/sync/errgroup"34
35"github.com/crossplane/crossplane-runtime/pkg/errors"36"github.com/crossplane/crossplane-runtime/pkg/logging"37
38"github.com/crossplane/crossplane/internal/xpkg"39"github.com/crossplane/crossplane/internal/xpkg/upbound"40"github.com/crossplane/crossplane/internal/xpkg/upbound/credhelper"41)
42
43const (44errGetwd = "failed to get working directory while searching for package"45errFindPackageinWd = "failed to find a package in current working directory"46errAnnotateLayers = "failed to propagate xpkg annotations from OCI image config file to image layers"47
48errFmtNewTag = "failed to parse package tag %q"49errFmtReadPackage = "failed to read package file %s"50errFmtPushPackage = "failed to push package file %s"51errFmtGetDigest = "failed to get digest of package file %s"52errFmtNewDigest = "failed to parse digest %q for package file %s"53errFmtGetMediaType = "failed to get media type of package file %s"54errFmtGetConfigFile = "failed to get OCI config file of package file %s"55errFmtWriteIndex = "failed to push an OCI image index of %d packages"56)
57
58// pushCmd pushes a package.
59type pushCmd struct {60// Arguments.61Package string `arg:"" help:"Where to push the package."`62
63// Flags. Keep sorted alphabetically.64PackageFiles []string `short:"f" type:"existingfile" placeholder:"PATH" help:"A comma-separated list of xpkg files to push."`65
66// Common Upbound API configuration.67upbound.Flags `embed:""`68
69// Internal state. These aren't part of the user-exposed CLI structure.70fs afero.Fs71}
72
73func (c *pushCmd) Help() string {74return `75Packages can be pushed to any OCI registry. Packages are pushed to the
76xpkg.upbound.io registry by default. A package's OCI tag must be a semantic
77version. Credentials for the registry are automatically retrieved from xpkg login
78and dockers configuration as fallback.
79
80Examples:
81
82# Push a multi-platform package.
83crossplane xpkg push -f function-amd64.xpkg,function-arm64.xpkg crossplane/function-example:v1.0.0
84
85# Push the xpkg file in the current directory to a different registry.
86crossplane xpkg push index.docker.io/crossplane/function-example:v1.0.0
87`
88}
89
90// AfterApply sets the tag for the parent push command.
91func (c *pushCmd) AfterApply() error {92c.fs = afero.NewOsFs()93return nil94}
95
96// Run runs the push cmd.
97func (c *pushCmd) Run(logger logging.Logger) error { //nolint:gocyclo // This feels easier to read as-is.98upCtx, err := upbound.NewFromFlags(c.Flags, upbound.AllowMissingProfile())99if err != nil {100return err101}102
103tag, err := name.NewTag(c.Package, name.WithDefaultRegistry(xpkg.DefaultRegistry))104if err != nil {105return errors.Wrapf(err, errFmtNewTag, c.Package)106}107
108// If package is not defined, attempt to find single package in current109// directory.110if len(c.PackageFiles) == 0 {111wd, err := os.Getwd()112if err != nil {113return errors.Wrap(err, errGetwd)114}115path, err := xpkg.FindXpkgInDir(c.fs, wd)116if err != nil {117return errors.Wrap(err, errFindPackageinWd)118}119c.PackageFiles = []string{path}120logger.Debug("Found package in directory", "path", path)121}122
123kc := authn.NewMultiKeychain(124authn.NewKeychainFromHelper(credhelper.New(125credhelper.WithLogger(logger),126credhelper.WithProfile(upCtx.ProfileName),127credhelper.WithDomain(upCtx.Domain.Hostname()),128)),129authn.DefaultKeychain,130)131
132// If there's only one package file, handle the simple path.133if len(c.PackageFiles) == 1 {134img, err := tarball.ImageFromPath(c.PackageFiles[0], nil)135if err != nil {136return errors.Wrapf(err, errFmtReadPackage, c.PackageFiles[0])137}138img, err = xpkg.AnnotateLayers(img)139if err != nil {140return errors.Wrapf(err, errAnnotateLayers)141}142if err := remote.Write(tag, img, remote.WithAuthFromKeychain(kc)); err != nil {143return errors.Wrapf(err, errFmtPushPackage, c.PackageFiles[0])144}145logger.Debug("Pushed package", "path", c.PackageFiles[0], "ref", tag.String())146return nil147}148
149// If there's more than one package file we'll write (push) them all by150// their digest, and create an index with the specified tag. This pattern is151// typically used to create a multi-platform image.152adds := make([]mutate.IndexAddendum, len(c.PackageFiles))153g, ctx := errgroup.WithContext(context.Background())154for i, file := range c.PackageFiles {155i, file := i, file // Pin range variables for use in goroutine156g.Go(func() error {157img, err := tarball.ImageFromPath(filepath.Clean(file), nil)158if err != nil {159return errors.Wrapf(err, errFmtReadPackage, file)160}161
162img, err = xpkg.AnnotateLayers(img)163if err != nil {164return errors.Wrapf(err, errAnnotateLayers)165}166
167d, err := img.Digest()168if err != nil {169return errors.Wrapf(err, errFmtGetDigest, file)170}171n := fmt.Sprintf("%s@%s", tag.Repository.Name(), d.String())172ref, err := name.NewDigest(n, name.WithDefaultRegistry(xpkg.DefaultRegistry))173if err != nil {174return errors.Wrapf(err, errFmtNewDigest, n, file)175}176
177mt, err := img.MediaType()178if err != nil {179return errors.Wrapf(err, errFmtGetMediaType, file)180}181
182conf, err := img.ConfigFile()183if err != nil {184return errors.Wrapf(err, errFmtGetConfigFile, file)185}186
187adds[i] = mutate.IndexAddendum{188Add: img,189Descriptor: v1.Descriptor{190MediaType: mt,191Platform: &v1.Platform{192Architecture: conf.Architecture,193OS: conf.OS,194OSVersion: conf.OSVersion,195},196},197}198if err := remote.Write(ref, img, remote.WithAuthFromKeychain(kc), remote.WithContext(ctx)); err != nil {199return errors.Wrapf(err, errFmtPushPackage, file)200}201logger.Debug("Pushed package", "path", file, "ref", ref.String())202return nil203})204}205
206if err := g.Wait(); err != nil {207return err208}209
210if err := remote.WriteIndex(tag, mutate.AppendManifests(empty.Index, adds...), remote.WithAuthFromKeychain(kc)); err != nil {211return errors.Wrapf(err, errFmtWriteIndex, len(adds))212}213logger.Debug("Wrote OCI index", "ref", tag.String(), "manifests", len(adds))214return nil215}
216