Skip to content

Commit

Permalink
build: add support for --push
Browse files Browse the repository at this point in the history
Signed-off-by: danishprakash <[email protected]>
  • Loading branch information
danishprakash committed Apr 25, 2023
1 parent a8ba52d commit d6925cd
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 52 deletions.
21 changes: 2 additions & 19 deletions cmd/buildah/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions define/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions define/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions imagebuildah/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type Executor struct {
registry string
ignoreUnrecognizedInstructions bool
quiet bool
push bool
runtime string
runtimeArgs []string
transientMounts []Mount
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions imagebuildah/stage_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
49 changes: 36 additions & 13 deletions internal/util/util.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -60,15 +62,35 @@ 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)
if err != nil {
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
Expand All @@ -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
}

Expand Down
10 changes: 9 additions & 1 deletion pkg/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,21 @@ 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
}
if buildOption.IsStdout {
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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type BudResults struct {
Pull string
PullAlways bool
PullNever bool
Push bool
Quiet bool
IdentityLabel bool
Rm bool
Expand Down Expand Up @@ -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")
Expand Down
38 changes: 21 additions & 17 deletions pkg/parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,25 +576,23 @@ 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
return define.BuildOutputOption{Path: "",
IsDir: false,
IsStdout: true}, nil
}
if !strings.Contains(buildOutput, ",") {
// expect default --output <dirname>
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)
Expand All @@ -607,29 +605,35 @@ 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":
if pathSelected {
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)
}
Expand All @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit d6925cd

Please sign in to comment.