summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorValentin Rothberg <rothberg@redhat.com>2020-04-14 13:31:29 +0200
committerValentin Rothberg <rothberg@redhat.com>2020-04-17 10:26:50 +0200
commit60624f948bf0067059f3d05e1bdc54589a9911e9 (patch)
tree995b11c25b4abaef99ced55cf1eaf4ed4df73bb0 /cmd
parent0d2b5532c417c58bd24e71a56c5c55b43e423a59 (diff)
downloadpodman-60624f948bf0067059f3d05e1bdc54589a9911e9.tar.gz
podman-60624f948bf0067059f3d05e1bdc54589a9911e9.tar.bz2
podman-60624f948bf0067059f3d05e1bdc54589a9911e9.zip
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 <rothberg@redhat.com>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/podman/build.go470
-rw-r--r--cmd/podman/utils/utils.go22
2 files changed, 492 insertions, 0 deletions
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()
+}