diff --git a/cmd/buildah/push.go b/cmd/buildah/push.go index c2706a48440..fc74ee43c13 100644 --- a/cmd/buildah/push.go +++ b/cmd/buildah/push.go @@ -18,7 +18,6 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/transports" - "github.com/containers/image/v5/transports/alltransports" "github.com/containers/storage" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" @@ -147,25 +146,9 @@ func pushCmd(c *cobra.Command, args []string, iopts pushOptions) error { return err } - dest, err := alltransports.ParseImageName(destSpec) - // add the docker:// transport to see if they neglected it. + dest, err := util.StringToImageReference(destSpec) if err != nil { - destTransport := strings.Split(destSpec, ":")[0] - if t := transports.Get(destTransport); t != nil { - return err - } - - if strings.Contains(destSpec, "://") { - return err - } - - destSpec = "docker://" + destSpec - dest2, err2 := alltransports.ParseImageName(destSpec) - if err2 != nil { - return err - } - dest = dest2 - logrus.Debugf("Assuming docker:// as the transport method for DESTINATION: %s", destSpec) + return fmt.Errorf("generating image reference: %w", err) } systemContext, err := parse.SystemContextFromOptions(c) diff --git a/define/build.go b/define/build.go index 42c8fd72e63..38fb6db78c8 100644 --- a/define/build.go +++ b/define/build.go @@ -163,6 +163,7 @@ type BuildOptions struct { // It allows end user to export recently built rootfs into a directory or tar. // See the documentation of 'buildah build --output' for the details of the format. BuildOutput string + Push bool // Additional tags to add to the image that we write, if we know of a // way to add them. AdditionalTags []string diff --git a/define/types.go b/define/types.go index 31482595983..958c095a062 100644 --- a/define/types.go +++ b/define/types.go @@ -101,11 +101,43 @@ type Secret struct { // BuildOutputOptions contains the the outcome of parsing the value of a build --output flag type BuildOutputOption struct { + Type BuildOutputType + Attrs map[string]string Path string // Only valid if !IsStdout IsDir bool IsStdout bool } +type BuildOutputType int + +const ( + Docker BuildOutputType = iota + Image + Local + Oci + Registry + Tar +) + +// String converts a BuildOutputType into a string. +func (t BuildOutputType) String() string { + switch t { + case Docker: + return "docker" + case Image: + return "image" + case Local: + return "local" + case Oci: + return "oci" + case Registry: + return "registry" + case Tar: + return "tar" + } + return fmt.Sprintf("unrecognized build output type %d", t) +} + // TempDirForURL checks if the passed-in string looks like a URL or -. If it is, // TempDirForURL creates a temporary directory, arranges for its contents to be // the contents of that URL, and returns the temporary directory's path, along diff --git a/imagebuildah/executor.go b/imagebuildah/executor.go index 7d9d0077b1b..d0f2064fd79 100644 --- a/imagebuildah/executor.go +++ b/imagebuildah/executor.go @@ -73,6 +73,7 @@ type Executor struct { registry string ignoreUnrecognizedInstructions bool quiet bool + push bool runtime string runtimeArgs []string transientMounts []Mount @@ -237,6 +238,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o registry: options.Registry, ignoreUnrecognizedInstructions: options.IgnoreUnrecognizedInstructions, quiet: options.Quiet, + push: options.Push, // TODO: not needed if planning to update buildOutput in cli/build runtime: options.Runtime, runtimeArgs: options.RuntimeArgs, transientMounts: transientMounts, diff --git a/imagebuildah/stage_executor.go b/imagebuildah/stage_executor.go index 54956b9c87c..3c5755b6b5b 100644 --- a/imagebuildah/stage_executor.go +++ b/imagebuildah/stage_executor.go @@ -1019,7 +1019,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, canGenerateBuildOutput := (s.executor.buildOutput != "" && lastStage) if canGenerateBuildOutput { logrus.Debugf("Generating custom build output with options %q", s.executor.buildOutput) - buildOutputOption, err = parse.GetBuildOutput(s.executor.buildOutput) + buildOutputOption, err = parse.GetBuildOutput(s.executor.buildOutput, s.executor.output) if err != nil { return "", nil, fmt.Errorf("failed to parse build output: %w", err) } @@ -2080,6 +2080,14 @@ func (s *StageExecutor) commit(ctx context.Context, createdBy string, emptyLayer } func (s *StageExecutor) generateBuildOutput(buildOutputOpts define.BuildOutputOption) error { + if buildOutputOpts.Type == define.Image { + err := internalUtil.ExportFromReader(nil, s.executor.store, buildOutputOpts) + if err != nil { + return fmt.Errorf("failed to export build output: %w", err) + } + return nil + } + extractRootfsOpts := buildah.ExtractRootfsOptions{} if unshare.IsRootless() { // In order to maintain as much parity as possible @@ -2099,7 +2107,7 @@ func (s *StageExecutor) generateBuildOutput(buildOutputOpts define.BuildOutputOp return fmt.Errorf("failed to extract rootfs from given container image: %w", err) } defer rc.Close() - err = internalUtil.ExportFromReader(rc, buildOutputOpts) + err = internalUtil.ExportFromReader(rc, s.executor.store, buildOutputOpts) if err != nil { return fmt.Errorf("failed to export build output: %w", err) } diff --git a/internal/util/util.go b/internal/util/util.go index c945ca85b8b..d91542ffc92 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,12 +1,14 @@ package util import ( + "context" "fmt" "io" "os" "path/filepath" "github.com/containers/buildah/define" + "github.com/containers/buildah/util" "github.com/containers/common/libimage" "github.com/containers/image/v5/types" encconfig "github.com/containers/ocicrypt/config" @@ -60,7 +62,7 @@ func GetTempDir() string { } // ExportFromReader reads bytes from given reader and exports to external tar, directory or stdout. -func ExportFromReader(input io.Reader, opts define.BuildOutputOption) error { +func ExportFromReader(input io.ReadCloser, store storage.Store, opts define.BuildOutputOption) error { var err error if !filepath.IsAbs(opts.Path) { opts.Path, err = filepath.Abs(opts.Path) @@ -68,7 +70,27 @@ func ExportFromReader(input io.Reader, opts define.BuildOutputOption) error { return err } } - if opts.IsDir { + if opts.Type == define.Image { + if opts.Attrs["push"] != "true" { + return nil + } + + image := opts.Attrs["name"] + destSpec := opts.Attrs["name"] + dest, err := util.StringToImageReference(destSpec) + if err != nil { + return fmt.Errorf("generating image reference: %w", err) + } + + libimageOptions := &libimage.PushOptions{} + libimageOptions.Writer = os.Stdout + runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: &types.SystemContext{}}) + destString := fmt.Sprintf("%s:%s", dest.Transport().Name(), dest.StringWithinTransport()) + _, err = runtime.Push(context.Background(), image, destString, libimageOptions) + if err != nil { + return fmt.Errorf("failed while pushing image %+q: %w", dest, err) + } + } else if opts.IsDir { // In order to keep this feature as close as possible to // buildkit it was decided to preserve ownership when // invoked as root since caller already has access to artifacts @@ -89,21 +111,22 @@ func ExportFromReader(input io.Reader, opts define.BuildOutputOption) error { err = chrootarchive.Untar(input, opts.Path, &archive.TarOptions{NoLchown: noLChown}) if err != nil { return fmt.Errorf("failed while performing untar at %q: %w", opts.Path, err) - } - } else { - outFile := os.Stdout - if !opts.IsStdout { - outFile, err = os.Create(opts.Path) + } else { + outFile := os.Stdout + if !opts.IsStdout { + outFile, err = os.Create(opts.Path) + if err != nil { + return fmt.Errorf("failed while creating destination tar at %q: %w", opts.Path, err) + } + defer outFile.Close() + } + _, err = io.Copy(outFile, input) if err != nil { - return fmt.Errorf("failed while creating destination tar at %q: %w", opts.Path, err) + return fmt.Errorf("failed while performing copy to %q: %w", opts.Path, err) } - defer outFile.Close() - } - _, err = io.Copy(outFile, input) - if err != nil { - return fmt.Errorf("failed while performing copy to %q: %w", opts.Path, err) } } + return nil } diff --git a/pkg/cli/build.go b/pkg/cli/build.go index 90aa9699f22..b1d961da829 100644 --- a/pkg/cli/build.go +++ b/pkg/cli/build.go @@ -292,7 +292,7 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) ( timestamp = &t } if c.Flag("output").Changed { - buildOption, err := parse.GetBuildOutput(iopts.BuildOutput) + buildOption, err := parse.GetBuildOutput(iopts.BuildOutput, output) if err != nil { return options, nil, nil, err } @@ -300,6 +300,13 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) ( iopts.Quiet = true } } + if c.Flag("push").Changed { + if len(iopts.BuildOutput) == 0 { + iopts.BuildOutput = "type=registry" + } else { + return options, nil, nil, fmt.Errorf("cannot set both --push and --output") + } + } var cacheTo []reference.Named var cacheFrom []reference.Named cacheTo = nil @@ -406,6 +413,7 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) ( Platforms: platforms, PullPolicy: pullPolicy, PullPushRetryDelay: pullPushRetryDelay, + Push: iopts.Push, Quiet: iopts.Quiet, RemoveIntermediateCtrs: iopts.Rm, ReportWriter: reporter, diff --git a/pkg/cli/common.go b/pkg/cli/common.go index 9c89456c36f..1b1864e863a 100644 --- a/pkg/cli/common.go +++ b/pkg/cli/common.go @@ -80,6 +80,7 @@ type BudResults struct { Pull string PullAlways bool PullNever bool + Push bool Quiet bool IdentityLabel bool Rm bool @@ -270,6 +271,7 @@ func GetBudFlags(flags *BudResults) pflag.FlagSet { fs.BoolVar(&flags.Stdin, "stdin", false, "pass stdin into containers") fs.StringArrayVarP(&flags.Tag, "tag", "t", []string{}, "tagged `name` to apply to the built image") fs.StringVarP(&flags.BuildOutput, "output", "o", "", "output destination (format: type=local,dest=path)") + fs.BoolVar(&flags.Push, "push", false, "Shorthand for `--output=type=registry`") fs.StringVar(&flags.Target, "target", "", "set the target build stage to build") fs.Int64Var(&flags.Timestamp, "timestamp", 0, "set created timestamp to the specified epoch seconds to allow for deterministic builds, defaults to current time") fs.BoolVar(&flags.TLSVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry") diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go index 8d02f59ddf5..d159d3ea73b 100644 --- a/pkg/parse/parse.go +++ b/pkg/parse/parse.go @@ -576,7 +576,7 @@ func AuthConfig(creds string) (*types.DockerAuthConfig, error) { // GetBuildOutput is responsible for parsing custom build output argument i.e `build --output` flag. // Takes `buildOutput` as string and returns BuildOutputOption -func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { +func GetBuildOutput(buildOutput, image string) (define.BuildOutputOption, error) { if len(buildOutput) == 1 && buildOutput == "-" { // Feature parity with buildkit, output tar to stdout // Read more here: https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs @@ -584,17 +584,15 @@ func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { IsDir: false, IsStdout: true}, nil } - if !strings.Contains(buildOutput, ",") { - // expect default --output - return define.BuildOutputOption{Path: buildOutput, - IsDir: true, - IsStdout: false}, nil + + out := define.BuildOutputOption{ + Attrs: map[string]string{}, + IsStdout: false, } + isDir := true - isStdout := false typeSelected := false pathSelected := false - path := "" tokens := strings.Split(buildOutput, ",") for _, option := range tokens { arr := strings.SplitN(option, "=", 2) @@ -607,11 +605,16 @@ func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { return define.BuildOutputOption{}, fmt.Errorf("duplicate %q not supported", arr[0]) } typeSelected = true - if arr[1] == "local" { - isDir = true - } else if arr[1] == "tar" { - isDir = false - } else { + switch arr[1] { + case define.Local.String(): + out.IsDir = true + case define.Tar.String(): + out.IsDir = false + case define.Registry.String(): + out.Type = define.Image + out.Attrs["push"] = "true" + out.Attrs["name"] = image + default: return define.BuildOutputOption{}, fmt.Errorf("invalid type %q selected for build output options %q", arr[1], buildOutput) } case "dest": @@ -619,17 +622,18 @@ func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { return define.BuildOutputOption{}, fmt.Errorf("duplicate %q not supported", arr[0]) } pathSelected = true - path = arr[1] + out.Path = arr[1] default: return define.BuildOutputOption{}, fmt.Errorf("unrecognized key %q in build output option: %q", arr[0], buildOutput) } } - if !typeSelected || !pathSelected { + if !typeSelected && !pathSelected { + // TODO: update error message return define.BuildOutputOption{}, fmt.Errorf("invalid build output option %q, accepted keys are type and dest must be present", buildOutput) } - if path == "-" { + if out.Path == "-" { if isDir { return define.BuildOutputOption{}, fmt.Errorf("invalid build output option %q, type=local and dest=- is not supported", buildOutput) } @@ -638,7 +642,7 @@ func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { IsStdout: true}, nil } - return define.BuildOutputOption{Path: path, IsDir: isDir, IsStdout: isStdout}, nil + return out, nil } // IDMappingOptions parses the build options related to user namespaces and ID mapping. diff --git a/util/util.go b/util/util.go index bec861483d4..7fb514fb8c3 100644 --- a/util/util.go +++ b/util/util.go @@ -19,6 +19,7 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/pkg/shortnames" "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/containers/storage" @@ -50,6 +51,31 @@ func StringInSlice(s string, slice []string) bool { return util.StringInSlice(s, slice) } +func StringToImageReference(image string) (types.ImageReference, error) { + dest, err := alltransports.ParseImageName(image) + // add the docker:// transport to see if they neglected it. + if err != nil { + destTransport := strings.Split(image, ":")[0] + if t := transports.Get(destTransport); t != nil { + return nil, err + } + + if strings.Contains(image, "://") { + return nil, err + } + + image = "docker://" + image + dest2, err2 := alltransports.ParseImageName(image) + if err2 != nil { + return nil, err + } + dest = dest2 + logrus.Debugf("Assuming docker:// as the transport method for DESTINATION: %s", image) + } + + return dest, nil +} + // resolveName checks if name is a valid image name, and if that name doesn't // include a domain portion, returns a list of the names which it might // correspond to in the set of configured registries, and the transport used to