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..ca401e17662 100644 --- a/define/types.go +++ b/define/types.go @@ -101,6 +101,8 @@ type Secret struct { // BuildOutputOptions contains the the outcome of parsing the value of a build --output flag type BuildOutputOption struct { + Type string + Attrs map[string]string Path string // Only valid if !IsStdout IsDir bool IsStdout bool 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..8cf31aff05b 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 == "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..d5d3f858aff 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,13 +1,17 @@ package util import ( + "context" "fmt" "io" "os" "path/filepath" + "strings" "github.com/containers/buildah/define" "github.com/containers/common/libimage" + "github.com/containers/image/v5/transports" + "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" encconfig "github.com/containers/ocicrypt/config" enchelpers "github.com/containers/ocicrypt/helpers" @@ -16,6 +20,7 @@ import ( "github.com/containers/storage/pkg/chrootarchive" "github.com/containers/storage/pkg/unshare" v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" ) // LookupImage returns *Image to corresponding imagename or id @@ -60,7 +65,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 +73,43 @@ func ExportFromReader(input io.Reader, opts define.BuildOutputOption) error { return err } } - if opts.IsDir { + if opts.Type == "image" { + if opts.Attrs["push"] != "true" { + return nil + } + + image := opts.Attrs["name"] + destSpec := opts.Attrs["name"] + dest, err := alltransports.ParseImageName(destSpec) + // add the docker:// transport to see if they neglected it. + 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) + } + + 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 +130,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..6e5e5868457 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,11 @@ 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" + } + } var cacheTo []reference.Named var cacheFrom []reference.Named cacheTo = nil @@ -406,6 +411,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..29ec5b31b32 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,22 @@ 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 + + // 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 +612,17 @@ 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 "local": + out.IsDir = true + case "tar": + out.IsDir = false + case "registry": + out.IsDir = true + out.Type = "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 +630,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 +650,8 @@ func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { IsStdout: true}, nil } - return define.BuildOutputOption{Path: path, IsDir: isDir, IsStdout: isStdout}, nil + return out, nil + // return define.BuildOutputOption{Path: path, IsDir: isDir, IsStdout: isStdout}, nil } // IDMappingOptions parses the build options related to user namespaces and ID mapping.