From 60624f948bf0067059f3d05e1bdc54589a9911e9 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Tue, 14 Apr 2020 13:31:29 +0200 Subject: podmanV2: implement build Implement `podman build` for the local client. The remote client will require some rather large work in the backend and a new build endpoint for the libpod rest API. Signed-off-by: Valentin Rothberg --- cmd/podman/build.go | 470 ++++++++++++++++++++++++++++++++++++ cmd/podman/utils/utils.go | 22 ++ pkg/domain/entities/engine_image.go | 1 + pkg/domain/entities/types.go | 12 + pkg/domain/infra/abi/images.go | 8 + pkg/domain/infra/tunnel/images.go | 4 + 6 files changed, 517 insertions(+) create mode 100644 cmd/podman/build.go create mode 100644 cmd/podman/utils/utils.go diff --git a/cmd/podman/build.go b/cmd/podman/build.go new file mode 100644 index 000000000..43a2f7ab5 --- /dev/null +++ b/cmd/podman/build.go @@ -0,0 +1,470 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + + "github.com/containers/buildah" + "github.com/containers/buildah/imagebuildah" + buildahCLI "github.com/containers/buildah/pkg/cli" + "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/docker/go-units" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// buildFlagsWrapper are local to cmd/ as the build code is using Buildah-internal +// types. Hence, after parsing, we are converting buildFlagsWrapper to the entities' +// options which essentially embed the Buildah types. +type buildFlagsWrapper struct { + // Buildah stuff first + buildahCLI.BudResults + buildahCLI.LayerResults + buildahCLI.FromAndBudResults + buildahCLI.NameSpaceResults + buildahCLI.UserNSResults + + // SquashAll squashes all layers into a single layer. + SquashAll bool +} + +var ( + // Command: podman _diff_ Object_ID + buildDescription = "Builds an OCI or Docker image using instructions from one or more Containerfiles and a specified build context directory." + buildCmd = &cobra.Command{ + Use: "build [flags] [CONTEXT]", + Short: "Build an image using instructions from Containerfiles", + Long: buildDescription, + TraverseChildren: true, + RunE: build, + Example: `podman build . + podman build --creds=username:password -t imageName -f Containerfile.simple . + podman build --layers --force-rm --tag imageName .`, + } + + buildOpts = buildFlagsWrapper{} +) + +// useLayers returns false if BUILDAH_LAYERS is set to "0" or "false" +// otherwise it returns true +func useLayers() string { + layers := os.Getenv("BUILDAH_LAYERS") + if strings.ToLower(layers) == "false" || layers == "0" { + return "false" + } + return "true" +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: buildCmd, + }) + flags := buildCmd.Flags() + + // Podman flags + flags.BoolVarP(&buildOpts.SquashAll, "squash-all", "", false, "Squash all layers into a single layer") + + // Bud flags + budFlags := buildahCLI.GetBudFlags(&buildOpts.BudResults) + // --pull flag + flag := budFlags.Lookup("pull") + if err := flag.Value.Set("true"); err != nil { + logrus.Errorf("unable to set --pull to true: %v", err) + } + flag.DefValue = "true" + flags.AddFlagSet(&budFlags) + + // Layer flags + layerFlags := buildahCLI.GetLayerFlags(&buildOpts.LayerResults) + // --layers flag + flag = layerFlags.Lookup("layers") + useLayersVal := useLayers() + if err := flag.Value.Set(useLayersVal); err != nil { + logrus.Errorf("unable to set --layers to %v: %v", useLayersVal, err) + } + flag.DefValue = useLayersVal + // --force-rm flag + flag = layerFlags.Lookup("force-rm") + if err := flag.Value.Set("true"); err != nil { + logrus.Errorf("unable to set --force-rm to true: %v", err) + } + flag.DefValue = "true" + flags.AddFlagSet(&layerFlags) + + // FromAndBud flags + fromAndBudFlags, err := buildahCLI.GetFromAndBudFlags(&buildOpts.FromAndBudResults, &buildOpts.UserNSResults, &buildOpts.NameSpaceResults) + if err != nil { + logrus.Errorf("error setting up build flags: %v", err) + os.Exit(1) + } + flags.AddFlagSet(&fromAndBudFlags) +} + +// build executes the build command. +func build(cmd *cobra.Command, args []string) error { + if (cmd.Flags().Changed("squash") && cmd.Flags().Changed("layers")) || + (cmd.Flags().Changed("squash-all") && cmd.Flags().Changed("layers")) || + (cmd.Flags().Changed("squash-all") && cmd.Flags().Changed("squash")) { + return errors.New("cannot specify --squash, --squash-all and --layers options together") + } + + contextDir, containerFiles, err := extractContextAndFiles(args, buildOpts.File) + if err != nil { + return err + } + + ie, err := registry.NewImageEngine(cmd, args) + if err != nil { + return err + } + + apiBuildOpts, err := buildFlagsWrapperToOptions(cmd, contextDir, &buildOpts) + if err != nil { + return err + } + + _, err = ie.Build(registry.GetContext(), containerFiles, *apiBuildOpts) + return err +} + +// extractContextAndFiles parses args and files to extract a context directory +// and {Container,Docker}files. +// +// TODO: this was copied and altered from the v1 client which in turn was +// copied and altered from the Buildah code. Ideally, all of this code should +// be cleanly consolidated into a package that is shared between Buildah and +// Podman. +func extractContextAndFiles(args, files []string) (string, []string, error) { + // Extract container files from the CLI (i.e., --file/-f) first. + var containerFiles []string + for _, f := range files { + if f == "-" { + containerFiles = append(containerFiles, "/dev/stdin") + } else { + containerFiles = append(containerFiles, f) + } + } + + // Determine context directory. + var contextDir string + if len(args) > 0 { + // The context directory could be a URL. Try to handle that. + tempDir, subDir, err := imagebuildah.TempDirForURL("", "buildah", args[0]) + if err != nil { + return "", nil, errors.Wrapf(err, "error prepping temporary context directory") + } + if tempDir != "" { + // We had to download it to a temporary directory. + // Delete it later. + defer func() { + if err = os.RemoveAll(tempDir); err != nil { + logrus.Errorf("error removing temporary directory %q: %v", contextDir, err) + } + }() + contextDir = filepath.Join(tempDir, subDir) + } else { + // Nope, it was local. Use it as is. + absDir, err := filepath.Abs(args[0]) + if err != nil { + return "", nil, errors.Wrapf(err, "error determining path to directory %q", args[0]) + } + contextDir = absDir + } + } else { + // No context directory or URL was specified. Try to use the home of + // the first locally-available Containerfile. + for i := range containerFiles { + if strings.HasPrefix(containerFiles[i], "http://") || + strings.HasPrefix(containerFiles[i], "https://") || + strings.HasPrefix(containerFiles[i], "git://") || + strings.HasPrefix(containerFiles[i], "github.com/") { + continue + } + absFile, err := filepath.Abs(containerFiles[i]) + if err != nil { + return "", nil, errors.Wrapf(err, "error determining path to file %q", containerFiles[i]) + } + contextDir = filepath.Dir(absFile) + break + } + } + + if contextDir == "" { + return "", nil, errors.Errorf("no context directory and no Containerfile specified") + } + if !utils.IsDir(contextDir) { + return "", nil, errors.Errorf("context must be a directory: %q", contextDir) + } + if len(containerFiles) == 0 { + if utils.FileExists(filepath.Join(contextDir, "Containerfile")) { + containerFiles = append(containerFiles, filepath.Join(contextDir, "Containerfile")) + } else { + containerFiles = append(containerFiles, filepath.Join(contextDir, "Dockerfile")) + } + } + + return contextDir, containerFiles, nil +} + +// buildFlagsWrapperToOptions converts the local build flags to the build options used +// in the API which embed Buildah types used across the build code. Doing the +// conversion here prevents the API from doing that (redundantly). +// +// TODO: this code should really be in Buildah. +func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *buildFlagsWrapper) (*entities.BuildOptions, error) { + output := "" + tags := []string{} + if c.Flag("tag").Changed { + tags = flags.Tag + if len(tags) > 0 { + output = tags[0] + tags = tags[1:] + } + } + + pullPolicy := imagebuildah.PullNever + if flags.Pull { + pullPolicy = imagebuildah.PullIfMissing + } + if flags.PullAlways { + pullPolicy = imagebuildah.PullAlways + } + + args := make(map[string]string) + if c.Flag("build-arg").Changed { + for _, arg := range flags.BuildArg { + av := strings.SplitN(arg, "=", 2) + if len(av) > 1 { + args[av[0]] = av[1] + } else { + delete(args, av[0]) + } + } + } + // Check to see if the BUILDAH_LAYERS environment variable is set and + // override command-line. + if _, ok := os.LookupEnv("BUILDAH_LAYERS"); ok { + flags.Layers = true + } + + // `buildah bud --layers=false` acts like `docker build --squash` does. + // That is all of the new layers created during the build process are + // condensed into one, any layers present prior to this build are + // retained without condensing. `buildah bud --squash` squashes both + // new and old layers down into one. Translate Podman commands into + // Buildah. Squash invoked, retain old layers, squash new layers into + // one. + if c.Flags().Changed("squash") && buildOpts.Squash { + flags.Squash = false + flags.Layers = false + } + // Squash-all invoked, squash both new and old layers into one. + if c.Flags().Changed("squash-all") { + flags.Squash = true + flags.Layers = false + } + + var stdin, stdout, stderr, reporter *os.File + stdin = os.Stdin + stdout = os.Stdout + stderr = os.Stderr + reporter = os.Stderr + + if c.Flag("logfile").Changed { + f, err := os.OpenFile(flags.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + return nil, errors.Errorf("error opening logfile %q: %v", flags.Logfile, err) + } + defer f.Close() + logrus.SetOutput(f) + stdout = f + stderr = f + reporter = f + } + + var memoryLimit, memorySwap int64 + var err error + if c.Flags().Changed("memory") { + memoryLimit, err = units.RAMInBytes(flags.Memory) + if err != nil { + return nil, err + } + } + + if c.Flags().Changed("memory-swap") { + memorySwap, err = units.RAMInBytes(flags.MemorySwap) + if err != nil { + return nil, err + } + } + + nsValues, err := getNsValues(flags) + if err != nil { + return nil, err + } + + networkPolicy := buildah.NetworkDefault + for _, ns := range nsValues { + if ns.Name == "none" { + networkPolicy = buildah.NetworkDisabled + break + } else if !filepath.IsAbs(ns.Path) { + networkPolicy = buildah.NetworkEnabled + break + } + } + + // `buildah bud --layers=false` acts like `docker build --squash` does. + // That is all of the new layers created during the build process are + // condensed into one, any layers present prior to this build are retained + // without condensing. `buildah bud --squash` squashes both new and old + // layers down into one. Translate Podman commands into Buildah. + // Squash invoked, retain old layers, squash new layers into one. + if c.Flags().Changed("squash") && flags.Squash { + flags.Squash = false + flags.Layers = false + } + // Squash-all invoked, squash both new and old layers into one. + if c.Flags().Changed("squash-all") { + flags.Squash = true + flags.Layers = false + } + + compression := imagebuildah.Gzip + if flags.DisableCompression { + compression = imagebuildah.Uncompressed + } + + isolation, err := parse.IsolationOption(flags.Isolation) + if err != nil { + return nil, errors.Wrapf(err, "error parsing ID mapping options") + } + + usernsOption, idmappingOptions, err := parse.IDMappingOptions(c, isolation) + if err != nil { + return nil, errors.Wrapf(err, "error parsing ID mapping options") + } + nsValues = append(nsValues, usernsOption...) + + systemContext, err := parse.SystemContextFromOptions(c) + if err != nil { + return nil, errors.Wrapf(err, "error building system context") + } + + format := "" + flags.Format = strings.ToLower(flags.Format) + switch { + case strings.HasPrefix(flags.Format, buildah.OCI): + format = buildah.OCIv1ImageManifest + case strings.HasPrefix(flags.Format, buildah.DOCKER): + format = buildah.Dockerv2ImageManifest + default: + return nil, errors.Errorf("unrecognized image type %q", flags.Format) + } + + runtimeFlags := []string{} + for _, arg := range flags.RuntimeFlags { + runtimeFlags = append(runtimeFlags, "--"+arg) + } + + // FIXME: the code below needs to be enabled (and adjusted) once the + // global/root flags are supported. + + // conf, err := runtime.GetConfig() + // if err != nil { + // return err + // } + // if conf != nil && conf.Engine.CgroupManager == config.SystemdCgroupsManager { + // runtimeFlags = append(runtimeFlags, "--systemd-cgroup") + // } + + opts := imagebuildah.BuildOptions{ + AddCapabilities: flags.CapAdd, + AdditionalTags: tags, + Annotations: flags.Annotation, + Architecture: flags.Arch, + Args: args, + BlobDirectory: flags.BlobCache, + CNIConfigDir: flags.CNIConfigDir, + CNIPluginPath: flags.CNIPlugInPath, + CommonBuildOpts: &buildah.CommonBuildOptions{ + AddHost: flags.AddHost, + CgroupParent: flags.CgroupParent, + CPUPeriod: flags.CPUPeriod, + CPUQuota: flags.CPUQuota, + CPUShares: flags.CPUShares, + CPUSetCPUs: flags.CPUSetCPUs, + CPUSetMems: flags.CPUSetMems, + Memory: memoryLimit, + MemorySwap: memorySwap, + ShmSize: flags.ShmSize, + Ulimit: flags.Ulimit, + Volumes: flags.Volumes, + }, + Compression: compression, + ConfigureNetwork: networkPolicy, + ContextDirectory: contextDir, + // DefaultMountsFilePath: FIXME: this requires global flags to be working! + Devices: flags.Devices, + DropCapabilities: flags.CapDrop, + Err: stderr, + ForceRmIntermediateCtrs: flags.ForceRm, + IDMappingOptions: idmappingOptions, + IIDFile: flags.Iidfile, + In: stdin, + Isolation: isolation, + Labels: flags.Label, + Layers: flags.Layers, + NamespaceOptions: nsValues, + NoCache: flags.NoCache, + OS: flags.OS, + Out: stdout, + Output: output, + OutputFormat: format, + PullPolicy: pullPolicy, + Quiet: flags.Quiet, + RemoveIntermediateCtrs: flags.Rm, + ReportWriter: reporter, + RuntimeArgs: runtimeFlags, + SignBy: flags.SignBy, + SignaturePolicyPath: flags.SignaturePolicy, + Squash: flags.Squash, + SystemContext: systemContext, + Target: flags.Target, + TransientMounts: flags.Volumes, + } + + return &entities.BuildOptions{BuildOptions: opts}, nil +} + +func getNsValues(flags *buildFlagsWrapper) ([]buildah.NamespaceOption, error) { + var ret []buildah.NamespaceOption + if flags.Network != "" { + switch { + case flags.Network == "host": + ret = append(ret, buildah.NamespaceOption{ + Name: string(specs.NetworkNamespace), + Host: true, + }) + case flags.Network == "container": + ret = append(ret, buildah.NamespaceOption{ + Name: string(specs.NetworkNamespace), + }) + case flags.Network[0] == '/': + ret = append(ret, buildah.NamespaceOption{ + Name: string(specs.NetworkNamespace), + Path: flags.Network, + }) + default: + return nil, errors.Errorf("unsupported configuration network=%s", flags.Network) + } + } + return ret, nil +} diff --git a/cmd/podman/utils/utils.go b/cmd/podman/utils/utils.go new file mode 100644 index 000000000..c7d105ba4 --- /dev/null +++ b/cmd/podman/utils/utils.go @@ -0,0 +1,22 @@ +package utils + +import "os" + +// IsDir returns true if the specified path refers to a directory. +func IsDir(path string) bool { + file, err := os.Stat(path) + if err != nil { + return false + } + return file.IsDir() +} + +// FileExists returns true if path refers to an existing file. +func FileExists(path string) bool { + file, err := os.Stat(path) + // All errors return file == nil + if err != nil { + return false + } + return !file.IsDir() +} diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index e3b606550..052e7bee5 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -7,6 +7,7 @@ import ( ) type ImageEngine interface { + Build(ctx context.Context, containerFiles []string, opts BuildOptions) (*BuildReport, error) Config(ctx context.Context) (*config.Config, error) Delete(ctx context.Context, nameOrId []string, opts ImageDeleteOptions) (*ImageDeleteReport, error) Diff(ctx context.Context, nameOrId string, options DiffOptions) (*DiffReport, error) diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index 31a05f5d3..e4e1c3ad2 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -4,6 +4,7 @@ import ( "errors" "net" + "github.com/containers/buildah/imagebuildah" "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/pkg/specgen" "github.com/containers/storage/pkg/archive" @@ -104,3 +105,14 @@ func (e ErrorModel) Cause() error { func (e ErrorModel) Code() int { return e.ResponseCode } + +// BuildOptions describe the options for building container images. +type BuildOptions struct { + imagebuildah.BuildOptions +} + +// BuildReport is the image-build report. +type BuildReport struct { + // ID of the image. + ID string +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 9467c14d4..0f710ad28 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -468,3 +468,11 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im func (ir *ImageEngine) Config(_ context.Context) (*config.Config, error) { return ir.Libpod.GetConfig() } + +func (ir *ImageEngine) Build(ctx context.Context, containerFiles []string, opts entities.BuildOptions) (*entities.BuildReport, error) { + id, _, err := ir.Libpod.Build(ctx, opts.BuildOptions, containerFiles...) + if err != nil { + return nil, err + } + return &entities.BuildReport{ID: id}, nil +} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 7d40e0327..6ea2bd9f2 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -259,3 +259,7 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im func (ir *ImageEngine) Config(_ context.Context) (*config.Config, error) { return config.Default() } + +func (ir *ImageEngine) Build(ctx context.Context, containerFiles []string, opts entities.BuildOptions) (*entities.BuildReport, error) { + return nil, errors.New("not implemented yet") +} -- cgit v1.2.3-54-g00ecf