diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go new file mode 100644 index 0000000000..e6d3aa5597 --- /dev/null +++ b/cmd/podman/common/build.go @@ -0,0 +1,605 @@ +package common + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + buildahDefine "github.com/containers/buildah/define" + buildahCLI "github.com/containers/buildah/pkg/cli" + "github.com/containers/buildah/pkg/parse" + buildahUtil "github.com/containers/buildah/pkg/util" + "github.com/containers/common/pkg/auth" + "github.com/containers/common/pkg/completion" + "github.com/containers/common/pkg/config" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + encconfig "github.com/containers/ocicrypt/config" + enchelpers "github.com/containers/ocicrypt/helpers" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/utils" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/env" + "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 + // Cleanup removes built images from remote connections on success + Cleanup bool +} + +func DefineBuildFlags(cmd *cobra.Command, buildOpts *BuildFlagsWrapper) { + flags := cmd.Flags() + + // buildx build --load ignored, but added for compliance + flags.Bool("load", false, "buildx --load") + _ = flags.MarkHidden("load") + + // buildx build --progress ignored, but added for compliance + flags.String("progress", "auto", "buildx --progress") + _ = flags.MarkHidden("progress") + + // 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" + flag.Usage = "Always attempt to pull the image (errors are fatal)" + flags.AddFlagSet(&budFlags) + + // Add the completion functions + budCompletions := buildahCLI.GetBudFlagsCompletions() + completion.CompleteCommandFlags(cmd, budCompletions) + + // Layer flags + layerFlags := buildahCLI.GetLayerFlags(&buildOpts.LayerResults) + // --layers flag + flag = layerFlags.Lookup("layers") + useLayersVal := useLayers() + buildOpts.Layers = useLayersVal == "true" + 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("Setting up build flags: %v", err) + os.Exit(1) + } + + flags.AddFlagSet(&fromAndBudFlags) + // Add the completion functions + fromAndBudFlagsCompletions := buildahCLI.GetFromAndBudFlagsCompletions() + completion.CompleteCommandFlags(cmd, fromAndBudFlagsCompletions) + flags.SetNormalizeFunc(buildahCLI.AliasFlags) + if registry.IsRemote() { + _ = flags.MarkHidden("disable-content-trust") + _ = flags.MarkHidden("sign-by") + _ = flags.MarkHidden("signature-policy") + _ = flags.MarkHidden("tls-verify") + _ = flags.MarkHidden("compress") + _ = flags.MarkHidden("output") + _ = flags.MarkHidden("logsplit") + _ = flags.MarkHidden("cw") + } +} + +func ParseBuildOpts(cmd *cobra.Command, args []string, buildOpts *BuildFlagsWrapper) (*entities.BuildOptions, error) { + if (cmd.Flags().Changed("squash") && cmd.Flags().Changed("layers")) || + (cmd.Flags().Changed("squash-all") && cmd.Flags().Changed("squash")) { + return nil, errors.New("cannot specify --squash with --layers and --squash-all with --squash") + } + + if cmd.Flag("output").Changed && registry.IsRemote() { + return nil, errors.New("'--output' option is not supported in remote mode") + } + + if buildOpts.Network == "none" { + if cmd.Flag("dns").Changed { + return nil, errors.New("the --dns option cannot be used with --network=none") + } + if cmd.Flag("dns-option").Changed { + return nil, errors.New("the --dns-option option cannot be used with --network=none") + } + if cmd.Flag("dns-search").Changed { + return nil, errors.New("the --dns-search option cannot be used with --network=none") + } + } + + if cmd.Flag("network").Changed { + if buildOpts.Network != "host" && buildOpts.Isolation == buildahDefine.IsolationChroot.String() { + return nil, fmt.Errorf("cannot set --network other than host with --isolation %s", buildOpts.Isolation) + } + } + + // Extract container files from the CLI (i.e., --file/-f) first. + var containerFiles []string + for _, f := range buildOpts.File { + if f == "-" { + if len(args) == 0 { + args = append(args, "-") + } else { + containerFiles = append(containerFiles, "/dev/stdin") + } + } else { + containerFiles = append(containerFiles, f) + } + } + + // Determine context directory. + var ( + contextDir string + apiBuildOpts entities.BuildOptions + ) + if len(args) > 0 { + // The context directory could be a URL. Try to handle that. + tempDir, subDir, err := buildahDefine.TempDirForURL("", "buildah", args[0]) + if err != nil { + return nil, fmt.Errorf("prepping temporary context directory: %w", err) + } + if tempDir != "" { + apiBuildOpts.TmpDirToClose = tempDir + 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, fmt.Errorf("determining path to directory %q: %w", args[0], err) + } + 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, fmt.Errorf("determining path to file %q: %w", containerFiles[i], err) + } + contextDir = filepath.Dir(absFile) + containerFiles[i] = absFile + break + } + } + + if contextDir == "" { + return nil, errors.New("no context directory and no Containerfile specified") + } + if !utils.IsDir(contextDir) { + return nil, fmt.Errorf("context must be a directory: %q", contextDir) + } + if len(containerFiles) == 0 { + switch { + case utils.FileExists(filepath.Join(contextDir, "Containerfile")): + if utils.IsDir(filepath.Join(contextDir, "Containerfile")) { + return nil, fmt.Errorf("containerfile: cannot be path or directory") + } + containerFiles = append(containerFiles, filepath.Join(contextDir, "Containerfile")) + case utils.FileExists(filepath.Join(contextDir, "Dockerfile")): + if utils.IsDir(filepath.Join(contextDir, "Dockerfile")) { + return nil, fmt.Errorf("dockerfile: cannot be path or directory") + } + containerFiles = append(containerFiles, filepath.Join(contextDir, "Dockerfile")) + default: + return nil, fmt.Errorf("no Containerfile or Dockerfile specified or found in context directory, %s: %w", contextDir, syscall.ENOENT) + } + } + + var logFile *os.File + if cmd.Flag("logfile").Changed { + var err error + logFile, err = os.OpenFile(buildOpts.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + return nil, err + } + apiBuildOpts.LogFileToClose = logFile + } + + buildahDefineOpts, err := buildFlagsWrapperToOptions(cmd, contextDir, buildOpts, logFile, buildOpts.Layers, buildOpts.Squash) + if err != nil { + return nil, err + } + apiBuildOpts.BuildOptions = *buildahDefineOpts + apiBuildOpts.ContainerFiles = containerFiles + + return &apiBuildOpts, err +} + +// 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, logfile *os.File, layers, squash bool) (*buildahDefine.BuildOptions, error) { + output := "" + tags := []string{} + if c.Flag("tag").Changed { + tags = flags.Tag + if len(tags) > 0 { + output = tags[0] + tags = tags[1:] + } + } + + if c.Flags().Changed("authfile") { + if err := auth.CheckAuthFile(flags.Authfile); err != nil { + return nil, err + } + } + + commonOpts, err := parse.CommonBuildOptions(c) + if err != nil { + return nil, err + } + + pullFlagsCount := 0 + if c.Flag("pull").Changed { + pullFlagsCount++ + } + if c.Flag("pull-always").Changed { + pullFlagsCount++ + } + if c.Flag("pull-never").Changed { + pullFlagsCount++ + } + + if pullFlagsCount > 1 { + return nil, errors.New("can only set one of 'pull' or 'pull-always' or 'pull-never'") + } + + // Allow for --pull, --pull=true, --pull=false, --pull=never, --pull=always + // --pull-always and --pull-never. The --pull-never and --pull-always options + // will not be documented. + pullPolicy := buildahDefine.PullIfMissing + if c.Flags().Changed("pull") && strings.EqualFold(strings.TrimSpace(flags.Pull), "true") { + pullPolicy = buildahDefine.PullAlways + } + if flags.PullAlways || strings.EqualFold(strings.TrimSpace(flags.Pull), "always") { + pullPolicy = buildahDefine.PullAlways + } + + if flags.PullNever || strings.EqualFold(strings.TrimSpace(flags.Pull), "never") { + pullPolicy = buildahDefine.PullNever + } + + var cleanTmpFile bool + flags.Authfile, cleanTmpFile = buildahUtil.MirrorToTempFileIfPathIsDescriptor(flags.Authfile) + if cleanTmpFile { + defer os.Remove(flags.Authfile) + } + + args := make(map[string]string) + if c.Flag("build-arg-file").Changed { + for _, argfile := range flags.BuildArgFile { + fargs, err := env.ParseFile(argfile) + if err != nil { + return nil, err + } + for name, val := range fargs { + args[name] = val + } + } + } + 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 { + // check if the env is set in the local environment and use that value if it is + if val, present := os.LookupEnv(av[0]); present { + args[av[0]] = val + } else { + delete(args, av[0]) + } + } + } + } + flags.Layers = layers + + // `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") && 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 + if !c.Flags().Changed("layers") { + // Buildah supports using layers and --squash together + // after https://github.com/containers/buildah/pull/3674 + // so podman must honor if user wants to still use layers + // with --squash-all. + flags.Layers = false + } + } + + var stdin io.Reader + if flags.Stdin { + stdin = os.Stdin + } + var stdout, stderr, reporter *os.File + stdout = os.Stdout + stderr = os.Stderr + reporter = os.Stderr + + if logfile != nil { + logrus.SetOutput(logfile) + stdout = logfile + stderr = logfile + reporter = logfile + } + + nsValues, networkPolicy, err := parse.NamespaceOptions(c) + if err != nil { + return nil, err + } + + compression := buildahDefine.Gzip + if flags.DisableCompression { + compression = buildahDefine.Uncompressed + } + + isolation, err := parse.IsolationOption(flags.Isolation) + if err != nil { + return nil, err + } + + usernsOption, idmappingOptions, err := parse.IDMappingOptions(c, isolation) + if err != nil { + return nil, err + } + nsValues = append(nsValues, usernsOption...) + + systemContext, err := parse.SystemContextFromOptions(c) + if err != nil { + return nil, err + } + + var format string + flags.Format = strings.ToLower(flags.Format) + switch { + case strings.HasPrefix(flags.Format, buildahDefine.OCI): + format = buildahDefine.OCIv1ImageManifest + case strings.HasPrefix(flags.Format, buildahDefine.DOCKER): + format = buildahDefine.Dockerv2ImageManifest + default: + return nil, fmt.Errorf("unrecognized image type %q", flags.Format) + } + + runtimeFlags := []string{} + for _, arg := range flags.RuntimeFlags { + runtimeFlags = append(runtimeFlags, "--"+arg) + } + + podmanConfig := registry.PodmanConfig() + for _, arg := range podmanConfig.RuntimeFlags { + runtimeFlags = append(runtimeFlags, "--"+arg) + } + if podmanConfig.ContainersConf.Engine.CgroupManager == config.SystemdCgroupsManager { + runtimeFlags = append(runtimeFlags, "--systemd-cgroup") + } + + platforms, err := parse.PlatformsFromOptions(c) + if err != nil { + return nil, err + } + + decConfig, err := getDecryptConfig(flags.DecryptionKeys) + if err != nil { + return nil, fmt.Errorf("unable to obtain decrypt config: %w", err) + } + + additionalBuildContext := make(map[string]*buildahDefine.AdditionalBuildContext) + if c.Flag("build-context").Changed { + for _, contextString := range flags.BuildContext { + av := strings.SplitN(contextString, "=", 2) + if len(av) > 1 { + parseAdditionalBuildContext, err := parse.GetAdditionalBuildContext(av[1]) + if err != nil { + return nil, fmt.Errorf("while parsing additional build context: %w", err) + } + additionalBuildContext[av[0]] = &parseAdditionalBuildContext + } else { + return nil, fmt.Errorf("while parsing additional build context: %q, accepts value in the form of key=value", av) + } + } + } + var cacheTo []reference.Named + var cacheFrom []reference.Named + if c.Flag("cache-to").Changed { + cacheTo, err = parse.RepoNamesToNamedReferences(flags.CacheTo) + if err != nil { + return nil, fmt.Errorf("unable to parse value provided `%s` to --cache-to: %w", flags.CacheTo, err) + } + } + if c.Flag("cache-from").Changed { + cacheFrom, err = parse.RepoNamesToNamedReferences(flags.CacheFrom) + if err != nil { + return nil, fmt.Errorf("unable to parse value provided `%s` to --cache-from: %w", flags.CacheTo, err) + } + } + var cacheTTL time.Duration + if c.Flag("cache-ttl").Changed { + cacheTTL, err = time.ParseDuration(flags.CacheTTL) + if err != nil { + return nil, fmt.Errorf("unable to parse value provided %q as --cache-ttl: %w", flags.CacheTTL, err) + } + } + + var confidentialWorkloadOptions buildahDefine.ConfidentialWorkloadOptions + if c.Flag("cw").Changed { + confidentialWorkloadOptions, err = parse.GetConfidentialWorkloadOptions(flags.CWOptions) + if err != nil { + return nil, err + } + } + + opts := buildahDefine.BuildOptions{ + AddCapabilities: flags.CapAdd, + AdditionalTags: tags, + AdditionalBuildContexts: additionalBuildContext, + AllPlatforms: flags.AllPlatforms, + Annotations: flags.Annotation, + Args: args, + BlobDirectory: flags.BlobCache, + BuildOutput: flags.BuildOutput, + CacheFrom: cacheFrom, + CacheTo: cacheTo, + CacheTTL: cacheTTL, + ConfidentialWorkload: confidentialWorkloadOptions, + CommonBuildOpts: commonOpts, + Compression: compression, + ConfigureNetwork: networkPolicy, + ContextDirectory: contextDir, + CPPFlags: flags.CPPFlags, + DefaultMountsFilePath: podmanConfig.ContainersConfDefaultsRO.Containers.DefaultMountsFile, + Devices: flags.Devices, + DropCapabilities: flags.CapDrop, + Envs: buildahCLI.LookupEnvVarReferences(flags.Envs, os.Environ()), + Err: stderr, + ForceRmIntermediateCtrs: flags.ForceRm, + From: flags.From, + GroupAdd: flags.GroupAdd, + IDMappingOptions: idmappingOptions, + In: stdin, + Isolation: isolation, + Jobs: &flags.Jobs, + Labels: flags.Label, + LayerLabels: flags.LayerLabel, + Layers: flags.Layers, + LogRusage: flags.LogRusage, + LogFile: flags.Logfile, + LogSplitByPlatform: flags.LogSplitByPlatform, + Manifest: flags.Manifest, + MaxPullPushRetries: 3, + NamespaceOptions: nsValues, + NoCache: flags.NoCache, + OSFeatures: flags.OSFeatures, + OSVersion: flags.OSVersion, + OciDecryptConfig: decConfig, + Out: stdout, + Output: output, + OutputFormat: format, + Platforms: platforms, + PullPolicy: pullPolicy, + PullPushRetryDelay: 2 * time.Second, + Quiet: flags.Quiet, + RemoveIntermediateCtrs: flags.Rm, + ReportWriter: reporter, + Runtime: podmanConfig.RuntimePath, + RuntimeArgs: runtimeFlags, + RusageLogFile: flags.RusageLogFile, + SignBy: flags.SignBy, + SignaturePolicyPath: flags.SignaturePolicy, + Squash: flags.Squash, + SystemContext: systemContext, + Target: flags.Target, + TransientMounts: flags.Volumes, + UnsetEnvs: flags.UnsetEnvs, + } + + if flags.IgnoreFile != "" { + excludes, err := parseDockerignore(flags.IgnoreFile) + if err != nil { + return nil, fmt.Errorf("unable to obtain decrypt config: %w", err) + } + opts.Excludes = excludes + } + + if c.Flag("timestamp").Changed { + timestamp := time.Unix(flags.Timestamp, 0).UTC() + opts.Timestamp = ×tamp + } + if c.Flag("skip-unused-stages").Changed { + opts.SkipUnusedStages = types.NewOptionalBool(flags.SkipUnusedStages) + } + + return &opts, nil +} + +// 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 getDecryptConfig(decryptionKeys []string) (*encconfig.DecryptConfig, error) { + decConfig := &encconfig.DecryptConfig{} + if len(decryptionKeys) > 0 { + // decryption + dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys) + if err != nil { + return nil, fmt.Errorf("invalid decryption keys: %w", err) + } + cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc}) + decConfig = cc.DecryptConfig + } + + return decConfig, nil +} + +func parseDockerignore(ignoreFile string) ([]string, error) { + excludes := []string{} + ignore, err := os.ReadFile(ignoreFile) + if err != nil { + return excludes, err + } + for _, e := range strings.Split(string(ignore), "\n") { + if len(e) == 0 || e[0] == '#' { + continue + } + excludes = append(excludes, e) + } + return excludes, nil +} diff --git a/cmd/podman/images/build.go b/cmd/podman/images/build.go index 104509c1f5..6bf7c732f7 100644 --- a/cmd/podman/images/build.go +++ b/cmd/podman/images/build.go @@ -2,50 +2,17 @@ package images import ( "errors" - "fmt" - "io" "os" "os/exec" - "path/filepath" - "strings" - "syscall" - "time" - buildahDefine "github.com/containers/buildah/define" buildahCLI "github.com/containers/buildah/pkg/cli" - "github.com/containers/buildah/pkg/parse" - buildahUtil "github.com/containers/buildah/pkg/util" - "github.com/containers/common/pkg/auth" - "github.com/containers/common/pkg/completion" - "github.com/containers/common/pkg/config" - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/types" - encconfig "github.com/containers/ocicrypt/config" - enchelpers "github.com/containers/ocicrypt/helpers" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/utils" - "github.com/containers/podman/v4/pkg/domain/entities" - "github.com/containers/podman/v4/pkg/env" "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." @@ -85,19 +52,9 @@ var ( podman buildx build --layers --force-rm --tag imageName .`, } - buildOpts = buildFlagsWrapper{} + buildOpts = common.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{ Command: buildCmd, @@ -117,203 +74,29 @@ func init() { } func buildFlags(cmd *cobra.Command) { - flags := cmd.Flags() - - // buildx build --load ignored, but added for compliance - flags.Bool("load", false, "buildx --load") - _ = flags.MarkHidden("load") - - // buildx build --progress ignored, but added for compliance - flags.String("progress", "auto", "buildx --progress") - _ = flags.MarkHidden("progress") - - // 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" - flag.Usage = "Always attempt to pull the image (errors are fatal)" - flags.AddFlagSet(&budFlags) - - // Add the completion functions - budCompletions := buildahCLI.GetBudFlagsCompletions() - completion.CompleteCommandFlags(cmd, budCompletions) - - // Layer flags - layerFlags := buildahCLI.GetLayerFlags(&buildOpts.LayerResults) - // --layers flag - flag = layerFlags.Lookup("layers") - useLayersVal := useLayers() - buildOpts.Layers = useLayersVal == "true" - 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("Setting up build flags: %v", err) - os.Exit(1) - } - - flags.AddFlagSet(&fromAndBudFlags) - // Add the completion functions - fromAndBudFlagsCompletions := buildahCLI.GetFromAndBudFlagsCompletions() - completion.CompleteCommandFlags(cmd, fromAndBudFlagsCompletions) - flags.SetNormalizeFunc(buildahCLI.AliasFlags) - if registry.IsRemote() { - _ = flags.MarkHidden("disable-content-trust") - _ = flags.MarkHidden("sign-by") - _ = flags.MarkHidden("signature-policy") - _ = flags.MarkHidden("tls-verify") - _ = flags.MarkHidden("compress") - _ = flags.MarkHidden("output") - _ = flags.MarkHidden("logsplit") - _ = flags.MarkHidden("cw") - } + common.DefineBuildFlags(cmd, &buildOpts) } // 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("squash")) { - return errors.New("cannot specify --squash with --layers and --squash-all with --squash") - } - - if cmd.Flag("output").Changed && registry.IsRemote() { - return errors.New("'--output' option is not supported in remote mode") - } - - if buildOpts.Network == "none" { - if cmd.Flag("dns").Changed { - return errors.New("the --dns option cannot be used with --network=none") - } - if cmd.Flag("dns-option").Changed { - return errors.New("the --dns-option option cannot be used with --network=none") - } - if cmd.Flag("dns-search").Changed { - return errors.New("the --dns-search option cannot be used with --network=none") - } - } - - if cmd.Flag("network").Changed { - if buildOpts.Network != "host" && buildOpts.Isolation == buildahDefine.IsolationChroot.String() { - return fmt.Errorf("cannot set --network other than host with --isolation %s", buildOpts.Isolation) - } - } - - // Extract container files from the CLI (i.e., --file/-f) first. - var containerFiles []string - for _, f := range buildOpts.File { - if f == "-" { - if len(args) == 0 { - args = append(args, "-") - } else { - 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 := buildahDefine.TempDirForURL("", "buildah", args[0]) - if err != nil { - return fmt.Errorf("prepping temporary context directory: %w", err) - } - 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("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 fmt.Errorf("determining path to directory %q: %w", args[0], err) - } - 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 fmt.Errorf("determining path to file %q: %w", containerFiles[i], err) - } - contextDir = filepath.Dir(absFile) - containerFiles[i] = absFile - break - } - } - - if contextDir == "" { - return errors.New("no context directory and no Containerfile specified") - } - if !utils.IsDir(contextDir) { - return fmt.Errorf("context must be a directory: %q", contextDir) - } - if len(containerFiles) == 0 { - switch { - case utils.FileExists(filepath.Join(contextDir, "Containerfile")): - if utils.IsDir(filepath.Join(contextDir, "Containerfile")) { - return fmt.Errorf("containerfile: cannot be path or directory") - } - containerFiles = append(containerFiles, filepath.Join(contextDir, "Containerfile")) - case utils.FileExists(filepath.Join(contextDir, "Dockerfile")): - if utils.IsDir(filepath.Join(contextDir, "Dockerfile")) { - return fmt.Errorf("dockerfile: cannot be path or directory") - } - containerFiles = append(containerFiles, filepath.Join(contextDir, "Dockerfile")) - default: - return fmt.Errorf("no Containerfile or Dockerfile specified or found in context directory, %s: %w", contextDir, syscall.ENOENT) - } - } - - var logfile *os.File - if cmd.Flag("logfile").Changed { - var err error - logfile, err = os.OpenFile(buildOpts.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) - if err != nil { - return err - } - defer logfile.Close() - } - - apiBuildOpts, err := buildFlagsWrapperToOptions(cmd, contextDir, &buildOpts, logfile) + apiBuildOpts, err := common.ParseBuildOpts(cmd, args, &buildOpts) if err != nil { return err } - report, err := registry.ImageEngine().Build(registry.GetContext(), containerFiles, *apiBuildOpts) + // Close the logFile if one was created based on the flag + if apiBuildOpts.LogFileToClose != nil { + defer apiBuildOpts.LogFileToClose.Close() + } + if apiBuildOpts.TmpDirToClose != "" { + // We had to download the context directory. + // Delete it later. + defer func() { + if err = os.RemoveAll(apiBuildOpts.TmpDirToClose); err != nil { + logrus.Errorf("Removing temporary directory %q: %v", apiBuildOpts.ContextDirectory, err) + } + }() + } + report, err := registry.ImageEngine().Build(registry.GetContext(), apiBuildOpts.ContainerFiles, *apiBuildOpts) if err != nil { exitCode := buildahCLI.ExecErrorCodeGeneric @@ -347,352 +130,3 @@ func build(cmd *cobra.Command, args []string) error { return 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, logfile *os.File) (*entities.BuildOptions, error) { - output := "" - tags := []string{} - if c.Flag("tag").Changed { - tags = flags.Tag - if len(tags) > 0 { - output = tags[0] - tags = tags[1:] - } - } - - if c.Flags().Changed("authfile") { - if err := auth.CheckAuthFile(flags.Authfile); err != nil { - return nil, err - } - } - - commonOpts, err := parse.CommonBuildOptions(c) - if err != nil { - return nil, err - } - - pullFlagsCount := 0 - if c.Flag("pull").Changed { - pullFlagsCount++ - } - if c.Flag("pull-always").Changed { - pullFlagsCount++ - } - if c.Flag("pull-never").Changed { - pullFlagsCount++ - } - - if pullFlagsCount > 1 { - return nil, errors.New("can only set one of 'pull' or 'pull-always' or 'pull-never'") - } - - // Allow for --pull, --pull=true, --pull=false, --pull=never, --pull=always - // --pull-always and --pull-never. The --pull-never and --pull-always options - // will not be documented. - pullPolicy := buildahDefine.PullIfMissing - if c.Flags().Changed("pull") && strings.EqualFold(strings.TrimSpace(flags.Pull), "true") { - pullPolicy = buildahDefine.PullAlways - } - if flags.PullAlways || strings.EqualFold(strings.TrimSpace(flags.Pull), "always") { - pullPolicy = buildahDefine.PullAlways - } - - if flags.PullNever || strings.EqualFold(strings.TrimSpace(flags.Pull), "never") { - pullPolicy = buildahDefine.PullNever - } - - var cleanTmpFile bool - flags.Authfile, cleanTmpFile = buildahUtil.MirrorToTempFileIfPathIsDescriptor(flags.Authfile) - if cleanTmpFile { - defer os.Remove(flags.Authfile) - } - - args := make(map[string]string) - if c.Flag("build-arg-file").Changed { - for _, argfile := range flags.BuildArgFile { - fargs, err := env.ParseFile(argfile) - if err != nil { - return nil, err - } - for name, val := range fargs { - args[name] = val - } - } - } - 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 { - // check if the env is set in the local environment and use that value if it is - if val, present := os.LookupEnv(av[0]); present { - args[av[0]] = val - } else { - delete(args, av[0]) - } - } - } - } - flags.Layers = buildOpts.Layers - - // `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 - if !c.Flags().Changed("layers") { - // Buildah supports using layers and --squash together - // after https://github.com/containers/buildah/pull/3674 - // so podman must honor if user wants to still use layers - // with --squash-all. - flags.Layers = false - } - } - - var stdin io.Reader - if flags.Stdin { - stdin = os.Stdin - } - var stdout, stderr, reporter *os.File - stdout = os.Stdout - stderr = os.Stderr - reporter = os.Stderr - - if logfile != nil { - logrus.SetOutput(logfile) - stdout = logfile - stderr = logfile - reporter = logfile - } - - nsValues, networkPolicy, err := parse.NamespaceOptions(c) - if err != nil { - return nil, err - } - - compression := buildahDefine.Gzip - if flags.DisableCompression { - compression = buildahDefine.Uncompressed - } - - isolation, err := parse.IsolationOption(flags.Isolation) - if err != nil { - return nil, err - } - - usernsOption, idmappingOptions, err := parse.IDMappingOptions(c, isolation) - if err != nil { - return nil, err - } - nsValues = append(nsValues, usernsOption...) - - systemContext, err := parse.SystemContextFromOptions(c) - if err != nil { - return nil, err - } - - var format string - flags.Format = strings.ToLower(flags.Format) - switch { - case strings.HasPrefix(flags.Format, buildahDefine.OCI): - format = buildahDefine.OCIv1ImageManifest - case strings.HasPrefix(flags.Format, buildahDefine.DOCKER): - format = buildahDefine.Dockerv2ImageManifest - default: - return nil, fmt.Errorf("unrecognized image type %q", flags.Format) - } - - runtimeFlags := []string{} - for _, arg := range flags.RuntimeFlags { - runtimeFlags = append(runtimeFlags, "--"+arg) - } - - podmanConfig := registry.PodmanConfig() - for _, arg := range podmanConfig.RuntimeFlags { - runtimeFlags = append(runtimeFlags, "--"+arg) - } - if podmanConfig.ContainersConf.Engine.CgroupManager == config.SystemdCgroupsManager { - runtimeFlags = append(runtimeFlags, "--systemd-cgroup") - } - - platforms, err := parse.PlatformsFromOptions(c) - if err != nil { - return nil, err - } - - decConfig, err := getDecryptConfig(flags.DecryptionKeys) - if err != nil { - return nil, fmt.Errorf("unable to obtain decrypt config: %w", err) - } - - additionalBuildContext := make(map[string]*buildahDefine.AdditionalBuildContext) - if c.Flag("build-context").Changed { - for _, contextString := range flags.BuildContext { - av := strings.SplitN(contextString, "=", 2) - if len(av) > 1 { - parseAdditionalBuildContext, err := parse.GetAdditionalBuildContext(av[1]) - if err != nil { - return nil, fmt.Errorf("while parsing additional build context: %w", err) - } - additionalBuildContext[av[0]] = &parseAdditionalBuildContext - } else { - return nil, fmt.Errorf("while parsing additional build context: %q, accepts value in the form of key=value", av) - } - } - } - var cacheTo []reference.Named - var cacheFrom []reference.Named - if c.Flag("cache-to").Changed { - cacheTo, err = parse.RepoNamesToNamedReferences(flags.CacheTo) - if err != nil { - return nil, fmt.Errorf("unable to parse value provided `%s` to --cache-to: %w", flags.CacheTo, err) - } - } - if c.Flag("cache-from").Changed { - cacheFrom, err = parse.RepoNamesToNamedReferences(flags.CacheFrom) - if err != nil { - return nil, fmt.Errorf("unable to parse value provided `%s` to --cache-from: %w", flags.CacheTo, err) - } - } - var cacheTTL time.Duration - if c.Flag("cache-ttl").Changed { - cacheTTL, err = time.ParseDuration(flags.CacheTTL) - if err != nil { - return nil, fmt.Errorf("unable to parse value provided %q as --cache-ttl: %w", flags.CacheTTL, err) - } - } - - var confidentialWorkloadOptions buildahDefine.ConfidentialWorkloadOptions - if c.Flag("cw").Changed { - confidentialWorkloadOptions, err = parse.GetConfidentialWorkloadOptions(flags.CWOptions) - if err != nil { - return nil, err - } - } - - opts := buildahDefine.BuildOptions{ - AddCapabilities: flags.CapAdd, - AdditionalTags: tags, - AdditionalBuildContexts: additionalBuildContext, - AllPlatforms: flags.AllPlatforms, - Annotations: flags.Annotation, - Args: args, - BlobDirectory: flags.BlobCache, - BuildOutput: flags.BuildOutput, - CacheFrom: cacheFrom, - CacheTo: cacheTo, - CacheTTL: cacheTTL, - ConfidentialWorkload: confidentialWorkloadOptions, - CommonBuildOpts: commonOpts, - Compression: compression, - ConfigureNetwork: networkPolicy, - ContextDirectory: contextDir, - CPPFlags: flags.CPPFlags, - DefaultMountsFilePath: podmanConfig.ContainersConfDefaultsRO.Containers.DefaultMountsFile, - Devices: flags.Devices, - DropCapabilities: flags.CapDrop, - Envs: buildahCLI.LookupEnvVarReferences(flags.Envs, os.Environ()), - Err: stderr, - ForceRmIntermediateCtrs: flags.ForceRm, - From: flags.From, - GroupAdd: flags.GroupAdd, - IDMappingOptions: idmappingOptions, - In: stdin, - Isolation: isolation, - Jobs: &flags.Jobs, - Labels: flags.Label, - LayerLabels: flags.LayerLabel, - Layers: flags.Layers, - LogRusage: flags.LogRusage, - LogFile: flags.Logfile, - LogSplitByPlatform: flags.LogSplitByPlatform, - Manifest: flags.Manifest, - MaxPullPushRetries: 3, - NamespaceOptions: nsValues, - NoCache: flags.NoCache, - OSFeatures: flags.OSFeatures, - OSVersion: flags.OSVersion, - OciDecryptConfig: decConfig, - Out: stdout, - Output: output, - OutputFormat: format, - Platforms: platforms, - PullPolicy: pullPolicy, - PullPushRetryDelay: 2 * time.Second, - Quiet: flags.Quiet, - RemoveIntermediateCtrs: flags.Rm, - ReportWriter: reporter, - Runtime: podmanConfig.RuntimePath, - RuntimeArgs: runtimeFlags, - RusageLogFile: flags.RusageLogFile, - SignBy: flags.SignBy, - SignaturePolicyPath: flags.SignaturePolicy, - Squash: flags.Squash, - SystemContext: systemContext, - Target: flags.Target, - TransientMounts: flags.Volumes, - UnsetEnvs: flags.UnsetEnvs, - } - - if flags.IgnoreFile != "" { - excludes, err := parseDockerignore(flags.IgnoreFile) - if err != nil { - return nil, fmt.Errorf("unable to obtain decrypt config: %w", err) - } - opts.Excludes = excludes - } - - if c.Flag("timestamp").Changed { - timestamp := time.Unix(flags.Timestamp, 0).UTC() - opts.Timestamp = ×tamp - } - if c.Flag("skip-unused-stages").Changed { - opts.SkipUnusedStages = types.NewOptionalBool(flags.SkipUnusedStages) - } - - return &entities.BuildOptions{BuildOptions: opts}, nil -} - -func getDecryptConfig(decryptionKeys []string) (*encconfig.DecryptConfig, error) { - decConfig := &encconfig.DecryptConfig{} - if len(decryptionKeys) > 0 { - // decryption - dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys) - if err != nil { - return nil, fmt.Errorf("invalid decryption keys: %w", err) - } - cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc}) - decConfig = cc.DecryptConfig - } - - return decConfig, nil -} - -func parseDockerignore(ignoreFile string) ([]string, error) { - excludes := []string{} - ignore, err := os.ReadFile(ignoreFile) - if err != nil { - return excludes, err - } - for _, e := range strings.Split(string(ignore), "\n") { - if len(e) == 0 || e[0] == '#' { - continue - } - excludes = append(excludes, e) - } - return excludes, nil -} diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index 44df664980..e8080b9ef4 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -2,6 +2,7 @@ package entities import ( "net" + "os" buildahDefine "github.com/containers/buildah/define" "github.com/containers/common/libnetwork/types" @@ -110,6 +111,11 @@ type ContainerCreateResponse struct { // BuildOptions describe the options for building container images. type BuildOptions struct { buildahDefine.BuildOptions + ContainerFiles []string + // Files that need to be closed after the build + // so need to pass this to the main build functions + LogFileToClose *os.File + TmpDirToClose string } // BuildReport is the image-build report.