diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go index bd79b8b960..6fee80eb45 100644 --- a/cmd/podman/common/build.go +++ b/cmd/podman/common/build.go @@ -171,6 +171,10 @@ func ParseBuildOpts(cmd *cobra.Command, args []string, buildOpts *BuildFlagsWrap } } + if buildOpts.StageLabels && !buildOpts.SaveStages { + return nil, errors.New(`"--stage-labels" requires "--save-stages"`) + } + // Extract container files from the CLI (i.e., --file/-f) first. var containerFiles []string for _, f := range buildOpts.File { @@ -605,11 +609,13 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *Buil Runtime: podmanConfig.RuntimePath, RuntimeArgs: runtimeFlags, RusageLogFile: flags.RusageLogFile, + SaveStages: flags.SaveStages, SBOMScanOptions: sbomScanOptions, SignBy: flags.SignBy, SignaturePolicyPath: flags.SignaturePolicy, SourcePolicyFile: flags.SourcePolicyFile, Squash: flags.Squash, + StageLabels: flags.StageLabels, SystemContext: systemContext, Target: flags.Target, TransientMounts: flags.Volumes, diff --git a/docs/source/markdown/options/save-stages.md b/docs/source/markdown/options/save-stages.md new file mode 100644 index 0000000000..ab22b0ffb4 --- /dev/null +++ b/docs/source/markdown/options/save-stages.md @@ -0,0 +1,12 @@ +####> This option file is used in: +####> podman build, farm build +####> If file is edited, make sure the changes +####> are applicable to all of those. +#### **--save-stages** + +Preserve intermediate stage images instead of removing them after the build completes (Default is `false`). By default, Buildah removes intermediate stage images to save space. +This option keeps those images, which can be useful for debugging multi-stage builds or for reusing intermediate stages in subsequent builds. + +`--save-stages` can be used with `--layers` and subsequent builds with `--layers` can use the preserved intermediate layers as cache. + +When combined with `--stage-labels`, all stage images (including the final image) will include metadata labels for easier identification and management. diff --git a/docs/source/markdown/options/stage-labels.md b/docs/source/markdown/options/stage-labels.md new file mode 100644 index 0000000000..1c2ef65b03 --- /dev/null +++ b/docs/source/markdown/options/stage-labels.md @@ -0,0 +1,17 @@ +####> This option file is used in: +####> podman build, farm build +####> If file is edited, make sure the changes +####> are applicable to all of those. +#### **--stage-labels** + +Add metadata labels to all intermediate stage images of the multistage build, +including the final image (Default is `false`). +This option requires `--save-stages` to be enabled. + +When enabled, all intermediate stage images and final image will be labeled with: + - `io.buildah.stage.name`: The stage alias (from `FROM ... AS alias`), or stage position + if no alias is specified + - `io.buildah.stage.base`: The base image used by this stage (pullspec or image ID + when stage uses another stage as base) + +These labels make it easier to identify, query, and manage images from multi-stage builds. diff --git a/docs/source/markdown/podman-build.1.md.in b/docs/source/markdown/podman-build.1.md.in index e4905a138d..6894190117 100644 --- a/docs/source/markdown/podman-build.1.md.in +++ b/docs/source/markdown/podman-build.1.md.in @@ -353,6 +353,8 @@ the help of emulation provided by packages like `qemu-user-static`. @@option runtime-flag +@@option save-stages + #### **--sbom**=*preset* Generate SBOMs (Software Bills Of Materials) for the output image by scanning @@ -466,6 +468,8 @@ Sign the image using a GPG key with the specified FINGERPRINT. (This option is n @@option ssh +@@option stage-labels + #### **--stdin** Pass stdin into the RUN containers. Sometime commands being RUN within a Containerfile diff --git a/docs/source/markdown/podman-farm-build.1.md.in b/docs/source/markdown/podman-farm-build.1.md.in index c77f15c363..2df5db3fb4 100644 --- a/docs/source/markdown/podman-farm-build.1.md.in +++ b/docs/source/markdown/podman-farm-build.1.md.in @@ -205,6 +205,8 @@ Build only on farm nodes that match the given platforms. @@option runtime-flag +@@option save-stages + @@option sbom @@option sbom-image-output @@ -239,6 +241,8 @@ Build only on farm nodes that match the given platforms. @@option ssh +@@option stage-labels + @@option tag @@option target diff --git a/internal/remote_build_helpers/utils.go b/internal/remote_build_helpers/utils.go index f0c1227300..cc5345157d 100644 --- a/internal/remote_build_helpers/utils.go +++ b/internal/remote_build_helpers/utils.go @@ -59,24 +59,3 @@ func (t *TempFileManager) CreateTempFileFromReader(dest string, pattern string, } return tmpFile.Name(), nil } - -// CreateTempSecret creates a temporary copy of a secret file in the specified -// context directory. The original secret file is copied to a new temporary file -// which is automatically added to the manager's cleanup list. -// -// Parameters: -// - secretPath: The path to the source secret file to copy -// - contextDir: The directory where the temporary secret file should be created -// -// Returns: -// - string: The path to the created temporary secret file -// - error: Any error encountered during the operation -func (t *TempFileManager) CreateTempSecret(secretPath, contextDir string) (string, error) { - secretFile, err := os.Open(secretPath) - if err != nil { - return "", fmt.Errorf("opening secret file %s: %w", secretPath, err) - } - defer secretFile.Close() - - return t.CreateTempFileFromReader(contextDir, "podman-build-secret-*", secretFile) -} diff --git a/internal/remote_build_helpers/utils_test.go b/internal/remote_build_helpers/utils_test.go index 03da24ffdb..f105fb18b9 100644 --- a/internal/remote_build_helpers/utils_test.go +++ b/internal/remote_build_helpers/utils_test.go @@ -2,7 +2,6 @@ package remote_build_helpers import ( "os" - "path/filepath" "strings" "testing" @@ -28,25 +27,4 @@ func TestTempFileManager(t *testing.T) { assert.NoFileExists(t, filename) }) - - t.Run("CreateTempSecret", func(t *testing.T) { - tempdir := t.TempDir() - secretPath := filepath.Join(tempdir, "secret.txt") - - content := "test secret" - err := os.WriteFile(secretPath, []byte(content), 0o600) - assert.NoError(t, err) - - filename, err := manager.CreateTempSecret(secretPath, tempdir) - assert.NoError(t, err) - assert.FileExists(t, filename) - - data, err := os.ReadFile(filename) - assert.NoError(t, err) - assert.Equal(t, content, string(data)) - - manager.Cleanup() - - assert.NoFileExists(t, filename) - }) } diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index d748ad4e53..e97036aa96 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -110,6 +110,7 @@ type BuildQuery struct { RewriteTimestamp bool `schema:"rewritetimestamp"` Retry int `schema:"retry"` RetryDelay string `schema:"retry-delay"` + SaveStages bool `schema:"save-stages"` Seccomp string `schema:"seccomp"` Secrets string `schema:"secrets"` SecurityOpt string `schema:"securityopt"` @@ -118,6 +119,7 @@ type BuildQuery struct { SourceDateEpoch int64 `schema:"sourcedateepoch"` SourcePolicy string `schema:"sourcePolicy"` Squash bool `schema:"squash"` + StageLabels bool `schema:"stage-labels"` TLSVerify bool `schema:"tlsVerify"` Tags []string `schema:"t"` Target string `schema:"target"` @@ -385,6 +387,10 @@ func createBuildOptions(query *BuildQuery, buildCtx *BuildContext, queryValues u compression := archive.Compression(query.Compression) + if query.StageLabels && !query.SaveStages { + return nil, nil, utils.GetGenericBadRequestError(errors.New("stage-labels requires save-stages be set as well")) + } + // Process tags tags := query.Tags var output string @@ -764,7 +770,9 @@ func createBuildOptions(query *BuildQuery, buildCtx *BuildContext, queryValues u RewriteTimestamp: query.RewriteTimestamp, RusageLogFile: query.RusageLogFile, SkipUnusedStages: skipUnusedStages, + SaveStages: query.SaveStages, Squash: query.Squash, + StageLabels: query.StageLabels, SystemContext: systemContext, Target: query.Target, TransientRunMounts: query.TransientRunMounts, diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 00b9e1bdbe..7b9df9bbd8 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -680,6 +680,22 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // Squash the resulting images layers into a single layer // (As of version 1.xx) // - in: query + // name: save-stages + // type: boolean + // default: false + // description: | + // Preserve intermediate stage images instead of removing them after the build completes. + // By default, they are removed to save space. + // However, they can be useful for debugging multi-stage builds or reusing stages in subsequent builds. + // - in: query + // name: stage-labels + // type: boolean + // default: false + // description: | + // Add metadata labels to all intermediate stage images of a multistage build, including the final image. + // If set to true, save-stages must also be set to true. + // If enabled, the labels 'io.buildah.stage.name' and 'io.buildah.stage.base' will be added. + // - in: query // name: labels // type: string // default: diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 1ff69db62b..515ee76a71 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -20,6 +20,7 @@ import ( "github.com/blang/semver/v4" "github.com/containers/buildah/define" + "github.com/containers/buildah/pkg/parse" "github.com/containers/podman/v6/internal/remote_build_helpers" ldefine "github.com/containers/podman/v6/libpod/define" "github.com/containers/podman/v6/pkg/auth" @@ -527,6 +528,13 @@ func prepareParams(options types.BuildOptions) (url.Values, error) { params.Add("unsetannotation", uannotation) } + if options.SaveStages { + params.Set("save-stages", "1") + } + if options.StageLabels { + params.Set("stage-labels", "1") + } + return params, nil } @@ -631,46 +639,24 @@ func prepareSecrets(secrets []string, contextDir string, tempManager *remote_bui secretsForRemote := []string{} tarContent := []string{} - for _, secret := range secrets { - secretOpt := strings.Split(secret, ",") - modifiedOpt := []string{} - for _, token := range secretOpt { - opt, val, hasVal := strings.Cut(token, "=") - if hasVal { - switch opt { - case "src": - // read specified secret into a tmp file - // move tmp file to tar and change secret source to relative tmp file - tmpSecretFilePath, err := tempManager.CreateTempSecret(val, contextDir) - if err != nil { - return nil, nil, err - } + parsed, err := parse.Secrets(secrets) + if err != nil { + return nil, nil, err + } - // add tmp file to context dir - tarContent = append(tarContent, tmpSecretFilePath) - - modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFilePath)) - modifiedOpt = append(modifiedOpt, modifiedSrc) - case "env": - // read specified env into a tmp file - // move tmp file to tar and change secret source to relative tmp file - secretVal := os.Getenv(val) - tmpSecretFilePath, err := tempManager.CreateTempFileFromReader(contextDir, "podman-build-secret-*", strings.NewReader(secretVal)) - if err != nil { - return nil, nil, err - } - - // add tmp file to context dir - tarContent = append(tarContent, tmpSecretFilePath) - - modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFilePath)) - modifiedOpt = append(modifiedOpt, modifiedSrc) - default: - modifiedOpt = append(modifiedOpt, token) - } - } + for _, secret := range parsed { + contents, err := secret.ResolveValue() + if err != nil { + return nil, nil, err } - secretsForRemote = append(secretsForRemote, strings.Join(modifiedOpt, ",")) + + tmpSecret, err := tempManager.CreateTempFileFromReader(contextDir, "podman-build-secret-*", bytes.NewReader(contents)) + if err != nil { + return nil, nil, err + } + + tarContent = append(tarContent, tmpSecret) + secretsForRemote = append(secretsForRemote, fmt.Sprintf("id=%s,src=%s", secret.ID, filepath.Base(tmpSecret))) } return secretsForRemote, tarContent, nil