spiegel_podman/cmd/podman/common/build.go
Urvashi Mohnani dd8f57a3b4 Add podman farm build command
Add podman farm build command that sends out builds to
nodes defined in the farm, builds the images on the farm
nodes, and pulls them back to the local machine to create
a manifest list.

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>
2023-10-24 12:58:39 -04:00

617 lines
20 KiB
Go

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
}
// FarmBuildHiddenFlags are the flags hidden from the farm build command because they are either not
// supported or don't make sense in the farm build use case
var FarmBuildHiddenFlags = []string{"arch", "all-platforms", "compress", "cw", "disable-content-trust",
"logsplit", "manifest", "os", "output", "platform", "sign-by", "signature-policy", "stdin", "tls-verify",
"variant"}
func DefineBuildFlags(cmd *cobra.Command, buildOpts *BuildFlagsWrapper, isFarmBuild bool) {
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")
flag.DefValue = "missing"
if err := flag.Value.Set("missing"); err != nil {
logrus.Errorf("Unable to set --pull to 'missing': %v", err)
}
flag.Usage = `Pull image policy ("always/true"|"missing"|"never/false"|"newer")`
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")
}
if isFarmBuild {
for _, f := range FarmBuildHiddenFlags {
_ = flags.MarkHidden(f)
}
}
}
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,
UnsetLabels: flags.UnsetLabels,
}
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 = &timestamp
}
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
}