build,config: add support for --unsetannotation

Just like `--unsetlabel` add support for `--unsetannotation`.

Closes: https://github.com/containers/buildah/issues/6183

Signed-off-by: flouthoc <flouthoc.git@gmail.com>
This commit is contained in:
flouthoc 2025-06-01 09:16:38 -07:00
parent b8d8cc375f
commit 83acf720d9
No known key found for this signature in database
GPG Key ID: F1993E9E45D69299
11 changed files with 180 additions and 24 deletions

View File

@ -49,6 +49,7 @@ type configResults struct {
volume []string volume []string
workingDir string workingDir string
unsetLabels []string unsetLabels []string
unsetAnnotations []string
} }
func init() { func init() {
@ -102,6 +103,7 @@ func init() {
flags.StringSliceVarP(&opts.volume, "volume", "v", []string{}, "add default `volume` path to be created for containers based on image (default [])") flags.StringSliceVarP(&opts.volume, "volume", "v", []string{}, "add default `volume` path to be created for containers based on image (default [])")
flags.StringVar(&opts.workingDir, "workingdir", "", "set working `directory` for containers based on image") flags.StringVar(&opts.workingDir, "workingdir", "", "set working `directory` for containers based on image")
flags.StringSliceVar(&opts.unsetLabels, "unsetlabel", nil, "remove image configuration label") flags.StringSliceVar(&opts.unsetLabels, "unsetlabel", nil, "remove image configuration label")
flags.StringSliceVar(&opts.unsetAnnotations, "unsetannotation", nil, "remove image configuration annotation")
rootCmd.AddCommand(configCommand) rootCmd.AddCommand(configCommand)
} }
@ -309,6 +311,10 @@ func updateConfig(builder *buildah.Builder, c *cobra.Command, iopts configResult
for _, key := range iopts.unsetLabels { for _, key := range iopts.unsetLabels {
builder.UnsetLabel(key) builder.UnsetLabel(key)
} }
// unset annotation if any
for _, key := range iopts.unsetAnnotations {
builder.UnsetAnnotation(key)
}
if c.Flag("workingdir").Changed { if c.Flag("workingdir").Changed {
builder.SetWorkDir(iopts.workingDir) builder.SetWorkDir(iopts.workingDir)
conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) WORKDIR %s", iopts.workingDir) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) WORKDIR %s", iopts.workingDir)

View File

@ -340,6 +340,8 @@ type BuildOptions struct {
UnsetEnvs []string UnsetEnvs []string
// UnsetLabels is a list of labels to not add to final image from base image. // UnsetLabels is a list of labels to not add to final image from base image.
UnsetLabels []string UnsetLabels []string
// UnsetAnnotations is a list of annotations to not add to final image from base image.
UnsetAnnotations []string
// Envs is a list of environment variables to set in the final image. // Envs is a list of environment variables to set in the final image.
Envs []string Envs []string
// OSFeatures specifies operating system features the image requires. // OSFeatures specifies operating system features the image requires.

View File

@ -1045,6 +1045,10 @@ include:
"sigpending": maximum number of pending signals (ulimit -i) "sigpending": maximum number of pending signals (ulimit -i)
"stack": maximum stack size (ulimit -s) "stack": maximum stack size (ulimit -s)
**--unsetannotation** *annotation*
Unset the image annotation, causing the annotation not to be inherited from the base image.
**--unsetenv** *env* **--unsetenv** *env*
Unset environment variables from the final image. Unset environment variables from the final image.

View File

@ -231,6 +231,10 @@ Note: this setting is not present in the OCIv1 image format, so it is discarded
Set default *stop signal* for container. This signal will be sent when container is stopped, default is SIGINT. Set default *stop signal* for container. This signal will be sent when container is stopped, default is SIGINT.
**--unsetannotation** *annotation*
Unset the image annotation, causing the annotation not to be inherited from the base image.
**--unsetlabel** *label* **--unsetlabel** *label*
Unset the image label, causing the label not to be inherited from the base image. Unset the image label, causing the label not to be inherited from the base image.

View File

@ -151,6 +151,7 @@ type Executor struct {
logPrefix string logPrefix string
unsetEnvs []string unsetEnvs []string
unsetLabels []string unsetLabels []string
unsetAnnotations []string
processLabel string // Shares processLabel of first stage container with containers of other stages in same build processLabel string // Shares processLabel of first stage container with containers of other stages in same build
mountLabel string // Shares mountLabel of first stage container with containers of other stages in same build mountLabel string // Shares mountLabel of first stage container with containers of other stages in same build
buildOutputs []string // Specifies instructions for any custom build output buildOutputs []string // Specifies instructions for any custom build output
@ -319,6 +320,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
logPrefix: logPrefix, logPrefix: logPrefix,
unsetEnvs: slices.Clone(options.UnsetEnvs), unsetEnvs: slices.Clone(options.UnsetEnvs),
unsetLabels: slices.Clone(options.UnsetLabels), unsetLabels: slices.Clone(options.UnsetLabels),
unsetAnnotations: slices.Clone(options.UnsetAnnotations),
buildOutputs: buildOutputs, buildOutputs: buildOutputs,
osVersion: options.OSVersion, osVersion: options.OSVersion,
osFeatures: slices.Clone(options.OSFeatures), osFeatures: slices.Clone(options.OSFeatures),
@ -331,6 +333,11 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
compatScratchConfig: options.CompatScratchConfig, compatScratchConfig: options.CompatScratchConfig,
noPivotRoot: options.NoPivotRoot, noPivotRoot: options.NoPivotRoot,
} }
// sort unsetAnnotations because we will later write these
// values to the history of the image therefore we want to
// make sure that order is always consistent.
slices.Sort(exec.unsetAnnotations)
if exec.err == nil { if exec.err == nil {
exec.err = os.Stderr exec.err = os.Stderr
} }

View File

@ -1284,7 +1284,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
if len(children) == 0 { if len(children) == 0 {
// There are no steps. // There are no steps.
if s.builder.FromImageID == "" || s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.labels) > 0 || len(s.executor.annotations) > 0 || len(s.executor.unsetEnvs) > 0 || len(s.executor.unsetLabels) > 0 || len(s.executor.sbomScanOptions) > 0 { if s.builder.FromImageID == "" || s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.labels) > 0 || len(s.executor.annotations) > 0 || len(s.executor.unsetEnvs) > 0 || len(s.executor.unsetLabels) > 0 || len(s.executor.sbomScanOptions) > 0 || len(s.executor.unsetAnnotations) > 0 {
// We either don't have a base image, or we need to // We either don't have a base image, or we need to
// transform the contents of the base image, or we need // transform the contents of the base image, or we need
// to make some changes to just the config blob. Whichever // to make some changes to just the config blob. Whichever
@ -1293,7 +1293,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// No base image means there's nothing to put in a // No base image means there's nothing to put in a
// layer, so don't create one. // layer, so don't create one.
emptyLayer := (s.builder.FromImageID == "") emptyLayer := (s.builder.FromImageID == "")
createdBy, err := s.getCreatedBy(nil, "") createdBy, err := s.getCreatedBy(nil, "", lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
} }
@ -1444,7 +1444,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
if s.executor.timestamp != nil { if s.executor.timestamp != nil {
timestamp = *s.executor.timestamp timestamp = *s.executor.timestamp
} }
createdBy, err := s.getCreatedBy(node, addedContentSummary) createdBy, err := s.getCreatedBy(node, addedContentSummary, false)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
} }
@ -1458,7 +1458,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// stage. // stage.
if lastStage || imageIsUsedLater { if lastStage || imageIsUsedLater {
logCommit(s.output, i) logCommit(s.output, i)
createdBy, err := s.getCreatedBy(node, addedContentSummary) createdBy, err := s.getCreatedBy(node, addedContentSummary, lastStage && lastInstruction)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
} }
@ -1533,7 +1533,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// cacheKey since it will be used either while pulling or pushing the // cacheKey since it will be used either while pulling or pushing the
// cache images. // cache images.
if needsCacheKey { if needsCacheKey {
cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err)
} }
@ -1561,13 +1561,13 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
addedContentSummary = s.getContentSummaryAfterAddingContent() addedContentSummary = s.getContentSummaryAfterAddingContent()
// regenerate cache key with updated content summary // regenerate cache key with updated content summary
if needsCacheKey { if needsCacheKey {
cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err)
} }
} }
} }
cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err)
} }
@ -1579,7 +1579,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// is ignored and will be automatically logged for --log-level debug // is ignored and will be automatically logged for --log-level debug
if ref, id, err := s.pullCache(ctx, cacheKey); ref != nil && id != "" && err == nil { if ref, id, err := s.pullCache(ctx, cacheKey); ref != nil && id != "" && err == nil {
logCachePulled(cacheKey, ref) logCachePulled(cacheKey, ref)
cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err)
} }
@ -1611,7 +1611,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
addedContentSummary = s.getContentSummaryAfterAddingContent() addedContentSummary = s.getContentSummaryAfterAddingContent()
// regenerate cache key with updated content summary // regenerate cache key with updated content summary
if needsCacheKey { if needsCacheKey {
cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err)
} }
@ -1620,7 +1620,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// Check if there's already an image based on our parent that // Check if there's already an image based on our parent that
// has the same change that we just made. // has the same change that we just made.
if checkForLayers && !avoidLookingCache { if checkForLayers && !avoidLookingCache {
cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err)
} }
@ -1633,7 +1633,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// is ignored and will be automatically logged for --log-level debug // is ignored and will be automatically logged for --log-level debug
if ref, id, err := s.pullCache(ctx, cacheKey); ref != nil && id != "" && err == nil { if ref, id, err := s.pullCache(ctx, cacheKey); ref != nil && id != "" && err == nil {
logCachePulled(cacheKey, ref) logCachePulled(cacheKey, ref)
cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err)
} }
@ -1683,7 +1683,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
// We're not going to find any more cache hits, so we // We're not going to find any more cache hits, so we
// can stop looking for them. // can stop looking for them.
checkForLayers = false checkForLayers = false
createdBy, err := s.getCreatedBy(node, addedContentSummary) createdBy, err := s.getCreatedBy(node, addedContentSummary, lastStage && lastInstruction)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
} }
@ -1725,7 +1725,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
if lastInstruction && lastStage { if lastInstruction && lastStage {
if s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.sbomScanOptions) != 0 { if s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.sbomScanOptions) != 0 {
createdBy, err := s.getCreatedBy(node, addedContentSummary) createdBy, err := s.getCreatedBy(node, addedContentSummary, lastStage && lastInstruction)
if err != nil { if err != nil {
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
} }
@ -1826,7 +1826,7 @@ func historyEntriesEqual(base, derived v1.History) bool {
// that we're comparing. // that we're comparing.
// Used to verify whether a cache of the intermediate image exists and whether // Used to verify whether a cache of the intermediate image exists and whether
// to run the build again. // to run the build again.
func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDiffIDs []digest.Digest, child *parser.Node, history []v1.History, diffIDs []digest.Digest, addedContentSummary string, buildAddsLayer bool) (bool, error) { func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDiffIDs []digest.Digest, child *parser.Node, history []v1.History, diffIDs []digest.Digest, addedContentSummary string, buildAddsLayer bool, lastInstruction bool) (bool, error) {
// our history should be as long as the base's, plus one entry for what // our history should be as long as the base's, plus one entry for what
// we're doing // we're doing
if len(history) != len(baseHistory)+1 { if len(history) != len(baseHistory)+1 {
@ -1865,7 +1865,7 @@ func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDif
return false, nil return false, nil
} }
} }
createdBy, err := s.getCreatedBy(child, addedContentSummary) createdBy, err := s.getCreatedBy(child, addedContentSummary, lastInstruction)
if err != nil { if err != nil {
return false, fmt.Errorf("unable to get createdBy for the node: %w", err) return false, fmt.Errorf("unable to get createdBy for the node: %w", err)
} }
@ -1875,15 +1875,25 @@ func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDif
// getCreatedBy returns the command the image at node will be created by. If // getCreatedBy returns the command the image at node will be created by. If
// the passed-in CompositeDigester is not nil, it is assumed to have the digest // the passed-in CompositeDigester is not nil, it is assumed to have the digest
// information for the content if the node is ADD or COPY. // information for the content if the node is ADD or COPY.
func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string) (string, error) { //
// This function acts differently if getCreatedBy is invoked by LastStep. For instances
// certain instructions like `removing annotations` does not makes sense for every step
// but only makes sense if the step is last step of a build.
func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string, isLastStep bool) (string, error) {
if node == nil { if node == nil {
return "/bin/sh", nil return "/bin/sh", nil
} }
inheritLabels := "" inheritLabels := ""
unsetAnnotations := ""
// If --inherit-label was manually set to false then update history. // If --inherit-label was manually set to false then update history.
if s.executor.inheritLabels == types.OptionalBoolFalse { if s.executor.inheritLabels == types.OptionalBoolFalse {
inheritLabels = "|inheritLabels=false" inheritLabels = "|inheritLabels=false"
} }
if isLastStep {
for _, annotation := range s.executor.unsetAnnotations {
unsetAnnotations += "|unsetAnnotation=" + annotation
}
}
switch strings.ToUpper(node.Value) { switch strings.ToUpper(node.Value) {
case "ARG": case "ARG":
for _, variable := range strings.Fields(node.Original) { for _, variable := range strings.Fields(node.Original) {
@ -1892,7 +1902,7 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
} }
} }
buildArgs := s.getBuildArgsKey() buildArgs := s.getBuildArgsKey()
return "/bin/sh -c #(nop) ARG " + buildArgs + inheritLabels, nil return "/bin/sh -c #(nop) ARG " + buildArgs + inheritLabels + unsetAnnotations, nil
case "RUN": case "RUN":
shArg := "" shArg := ""
buildArgs := s.getBuildArgsResolvedForRun() buildArgs := s.getBuildArgsResolvedForRun()
@ -1972,16 +1982,16 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
if buildArgs != "" { if buildArgs != "" {
result = result + "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " " result = result + "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " "
} }
result = result + "/bin/sh -c " + shArg + heredoc + appendCheckSum + inheritLabels result = result + "/bin/sh -c " + shArg + heredoc + appendCheckSum + inheritLabels + unsetAnnotations
return result, nil return result, nil
case "ADD", "COPY": case "ADD", "COPY":
destination := node destination := node
for destination.Next != nil { for destination.Next != nil {
destination = destination.Next destination = destination.Next
} }
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " " + inheritLabels, nil return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " " + inheritLabels + " " + unsetAnnotations, nil
default: default:
return "/bin/sh -c #(nop) " + node.Original + inheritLabels, nil return "/bin/sh -c #(nop) " + node.Original + inheritLabels + unsetAnnotations, nil
} }
} }
@ -2115,7 +2125,7 @@ func (s *StageExecutor) tagExistingImage(ctx context.Context, cacheID, output st
// generated CacheKey is further used by buildah to lock and decide // generated CacheKey is further used by buildah to lock and decide
// tag for the intermediate image which can be pushed and pulled to/from // tag for the intermediate image which can be pushed and pulled to/from
// the remote repository. // the remote repository.
func (s *StageExecutor) generateCacheKey(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool) (string, error) { func (s *StageExecutor) generateCacheKey(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool, lastInstruction bool) (string, error) {
hash := sha256.New() hash := sha256.New()
var baseHistory []v1.History var baseHistory []v1.History
var diffIDs []digest.Digest var diffIDs []digest.Digest
@ -2130,7 +2140,7 @@ func (s *StageExecutor) generateCacheKey(ctx context.Context, currNode *parser.N
fmt.Fprintln(hash, diffIDs[i].String()) fmt.Fprintln(hash, diffIDs[i].String())
} }
} }
createdBy, err := s.getCreatedBy(currNode, addedContentDigest) createdBy, err := s.getCreatedBy(currNode, addedContentDigest, lastInstruction)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -2248,7 +2258,7 @@ func (s *StageExecutor) pullCache(ctx context.Context, cacheKey string) (referen
// intermediateImageExists returns image ID if an intermediate image of currNode exists in the image store from a previous build. // intermediateImageExists returns image ID if an intermediate image of currNode exists in the image store from a previous build.
// It verifies this by checking the parent of the top layer of the image and the history. // It verifies this by checking the parent of the top layer of the image and the history.
// If more than one image matches as potiential candidates then priority is given to the most recently built image. // If more than one image matches as potiential candidates then priority is given to the most recently built image.
func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool) (string, error) { func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool, lastInstruction bool) (string, error) {
cacheCandidates := []storage.Image{} cacheCandidates := []storage.Image{}
// Get the list of images available in the image store // Get the list of images available in the image store
images, err := s.executor.store.Images() images, err := s.executor.store.Images()
@ -2315,7 +2325,7 @@ func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *p
continue continue
} }
// children + currNode is the point of the Dockerfile we are currently at. // children + currNode is the point of the Dockerfile we are currently at.
foundMatch, err := s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer) foundMatch, err := s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer, lastInstruction)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -2431,6 +2441,11 @@ func (s *StageExecutor) commit(ctx context.Context, createdBy string, emptyLayer
annotationk, annotationv, _ := strings.Cut(annotationSpec, "=") annotationk, annotationv, _ := strings.Cut(annotationSpec, "=")
s.builder.SetAnnotation(annotationk, annotationv) s.builder.SetAnnotation(annotationk, annotationv)
} }
if finalInstruction {
for _, key := range s.executor.unsetAnnotations {
s.builder.UnsetAnnotation(key)
}
}
if imageRef != nil { if imageRef != nil {
logName := transports.ImageName(imageRef) logName := transports.ImageName(imageRef)
logrus.Debugf("COMMIT %q", logName) logrus.Debugf("COMMIT %q", logName)

View File

@ -419,6 +419,7 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
TransientMounts: iopts.Volumes, TransientMounts: iopts.Volumes,
UnsetEnvs: iopts.UnsetEnvs, UnsetEnvs: iopts.UnsetEnvs,
UnsetLabels: iopts.UnsetLabels, UnsetLabels: iopts.UnsetLabels,
UnsetAnnotations: iopts.UnsetAnnotations,
} }
if iopts.RetryDelay != "" { if iopts.RetryDelay != "" {
options.PullPushRetryDelay, err = time.ParseDuration(iopts.RetryDelay) options.PullPushRetryDelay, err = time.ParseDuration(iopts.RetryDelay)

View File

@ -116,6 +116,7 @@ type BudResults struct {
RusageLogFile string RusageLogFile string
UnsetEnvs []string UnsetEnvs []string
UnsetLabels []string UnsetLabels []string
UnsetAnnotations []string
Envs []string Envs []string
OSFeatures []string OSFeatures []string
OSVersion string OSVersion string
@ -314,6 +315,7 @@ newer: only pull base and SBOM scanner images when newer images exist on the r
fs.String("variant", "", "override the `variant` of the specified image") fs.String("variant", "", "override the `variant` of the specified image")
fs.StringSliceVar(&flags.UnsetEnvs, "unsetenv", nil, "unset environment variable from final image") fs.StringSliceVar(&flags.UnsetEnvs, "unsetenv", nil, "unset environment variable from final image")
fs.StringSliceVar(&flags.UnsetLabels, "unsetlabel", nil, "unset label when inheriting labels from base image") fs.StringSliceVar(&flags.UnsetLabels, "unsetlabel", nil, "unset label when inheriting labels from base image")
fs.StringSliceVar(&flags.UnsetAnnotations, "unsetannotation", nil, "unset annotation when inheriting annotation from base image")
return fs return fs
} }
@ -368,6 +370,7 @@ func GetBudFlagsCompletions() commonComp.FlagCompletions {
flagCompletion["timestamp"] = commonComp.AutocompleteNone flagCompletion["timestamp"] = commonComp.AutocompleteNone
flagCompletion["unsetenv"] = commonComp.AutocompleteNone flagCompletion["unsetenv"] = commonComp.AutocompleteNone
flagCompletion["unsetlabel"] = commonComp.AutocompleteNone flagCompletion["unsetlabel"] = commonComp.AutocompleteNone
flagCompletion["unsetannotation"] = commonComp.AutocompleteNone
flagCompletion["variant"] = commonComp.AutocompleteNone flagCompletion["variant"] = commonComp.AutocompleteNone
return flagCompletion return flagCompletion
} }

View File

@ -2670,6 +2670,104 @@ _EOF
expect_output "$want_output" expect_output "$want_output"
} }
@test "bud and test --unsetannotation" {
base=registry.fedoraproject.org/fedora-minimal
_prefetch $base
target=exp
run_buildah --version
local -a output_fields=($output)
buildah_version=${output_fields[2]}
buildah inspect --format '{{ .ImageAnnotations }}' $base
not_want_output='map[]'
assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base"
annotations=$(buildah inspect --format '{{ range $key, $value := .ImageAnnotations }}{{ $key }} {{end}}' $base)
annotationflags="--annotation hello=world"
for annotation in $annotations; do
if test $annotation != io.buildah.version ; then
annotationflags="$annotationflags --unsetannotation $annotation"
fi
done
run_buildah build $WITH_POLICY_JSON $annotationflags -t $target --from $base $BUDFILES/base-with-labels
# no annotations should be inherited from base image
# and `hello=world` which we just added using cli flag
want_output='map["hello":"world"]'
run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target
expect_output "$want_output"
}
@test "bud and test --unsetannotation with --layers" {
base=registry.fedoraproject.org/fedora-minimal
_prefetch $base
target=exp
run_buildah --version
local -a output_fields=($output)
buildah_version=${output_fields[2]}
buildah inspect --format '{{ .ImageAnnotations }}' $base
not_want_output='map[]'
assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base"
## Build without removing annotations
run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid1 -t $target --from $base $BUDFILES/base-with-labels
## Second build must use cache
run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid2 -t $target --from $base $BUDFILES/base-with-labels
## Must use cache
expect_output --substring " Using cache"
cmp ${TEST_SCRATCH_DIR}/iid1 ${TEST_SCRATCH_DIR}/iid2
annotations=$(buildah inspect --format '{{ range $key, $value := .ImageAnnotations }}{{ $key }} {{end}}' $base)
annotationflags="--annotation hello=world"
for annotation in $annotations; do
if test $annotation != io.buildah.version ; then
annotationflags="$annotationflags --unsetannotation $annotation"
fi
done
## Since we are unsetting something, this should not use previous image present in the cache.
run_buildah build $WITH_POLICY_JSON --layers $annotationflags -t $target --from $base $BUDFILES/base-with-labels
## should not contain `Using Cache`
#assert "$output" !~ "Using cache"
# no annotations should be inherited from base image
# and `hello=world` which we just added using cli flag
want_output='map["hello":"world"]'
run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target
expect_output "$want_output"
}
@test "bud and test --unsetannotation with only base image" {
base=registry.fedoraproject.org/fedora-minimal
_prefetch $base
target=exp
run_buildah --version
local -a output_fields=($output)
buildah_version=${output_fields[2]}
buildah inspect --format '{{ .ImageAnnotations }}' $base
not_want_output='map[]'
assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base"
annotations=$(buildah inspect --format '{{ range $key, $value := .ImageAnnotations }}{{ $key }} {{end}}' $base)
annotationflags="--annotation hello=world"
for annotation in $annotations; do
if test $annotation != io.buildah.version ; then
annotationflags="$annotationflags --unsetannotation $annotation"
fi
done
run_buildah build $WITH_POLICY_JSON $annotationflags -t $target --from $base $BUDFILES/only-base
want_output='map["hello":"world"]'
run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target
expect_output "$want_output"
}
@test "bud and test inherit-labels" { @test "bud and test inherit-labels" {
base=registry.fedoraproject.org/fedora-minimal base=registry.fedoraproject.org/fedora-minimal
_prefetch $base _prefetch $base

View File

@ -0,0 +1 @@
FROM registry.fedoraproject.org/fedora-minimal

View File

@ -96,6 +96,21 @@ function check_matrix() {
assert "$output" == "" "name label should be removed" assert "$output" == "" "name label should be removed"
} }
@test "config --unsetannotation" {
base=registry.fedoraproject.org/fedora-minimal
_prefetch $base
run_buildah from --quiet --pull=false $WITH_POLICY_JSON $base
cid=$output
run_buildah commit $WITH_POLICY_JSON $cid with-name-annotation
run_buildah config --unsetannotation org.opencontainers.image.base.name $cid
run_buildah commit $WITH_POLICY_JSON $cid without-name-annotation
run_buildah inspect --format '{{ index .ImageAnnotations "org.opencontainers.image.base.name"}}' with-name-annotation
assert "$output" != "" "annotation should be set in base image"
run_buildah inspect --format '{{ index .ImageAnnotations "org.opencontainers.image.base.name"}}' without-name-annotation
assert "$output" == "" "name annotation should be removed"
}
@test "config set empty entrypoint doesn't wipe cmd" { @test "config set empty entrypoint doesn't wipe cmd" {
run_buildah from $WITH_POLICY_JSON scratch run_buildah from $WITH_POLICY_JSON scratch
cid=$output cid=$output