From efba9996f6e6edde344ad7cdde1ca7a217cd3758 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Mon, 23 Mar 2026 11:35:34 -0400 Subject: [PATCH] Implement `--save-stages`/`--stage-labels` for build These are two new Buildah flags that we need to wire into Podman (both local and remote) and document, with the interesting note that one requires the other and a check needed to be added for that. Also: secret parsing was tightened up in Buildah, and was breaking the remote build tests. Rewire it to use the new parser Buildah made, which ends up simplifying the code considerably. Tests are back to passing afterwards. Signed-off-by: Matthew Heon --- cmd/podman/common/build.go | 6 ++ docs/source/markdown/options/save-stages.md | 12 ++++ docs/source/markdown/options/stage-labels.md | 17 +++++ docs/source/markdown/podman-build.1.md.in | 4 ++ .../source/markdown/podman-farm-build.1.md.in | 4 ++ internal/remote_build_helpers/utils.go | 21 ------- internal/remote_build_helpers/utils_test.go | 22 ------- pkg/api/handlers/compat/images_build.go | 8 +++ pkg/api/server/register_images.go | 16 +++++ pkg/bindings/images/build.go | 62 +++++++------------ 10 files changed, 91 insertions(+), 81 deletions(-) create mode 100644 docs/source/markdown/options/save-stages.md create mode 100644 docs/source/markdown/options/stage-labels.md 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