mirror of
https://github.com/containers/podman.git
synced 2026-05-25 00:53:43 -04:00
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 <matthew.heon@pm.me>
This commit is contained in:
@@ -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,
|
||||
|
||||
12
docs/source/markdown/options/save-stages.md
Normal file
12
docs/source/markdown/options/save-stages.md
Normal file
@@ -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.
|
||||
17
docs/source/markdown/options/stage-labels.md
Normal file
17
docs/source/markdown/options/stage-labels.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user