Skip to content

Commit 2da6f4b

Browse files
committed
Handle duplicate stage nicknames
We haven't been correctly handling cases where multiple stages used the same nickname, in which case every reference to a previous stage should be treated as a reference to the most recent stage that uses that name, bug historically we've always treated them as references to the first stage that used a particular name. Resolving this generally means that a number of maps which were previously keyed by stage names need to only be keyed by stage indexes, with names being resolved to index values as late as possible. To that end, these map fields in the imagebuildah.executor type are being switched from using string keys to integer keys: * stages * stageImageIDs * imageMap * rootfsMap * terminatedStage Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
1 parent 9994690 commit 2da6f4b

File tree

3 files changed

+172
-94
lines changed

3 files changed

+172
-94
lines changed

imagebuildah/executor.go

Lines changed: 65 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ type executor struct {
7171
cacheTTL time.Duration
7272
containerSuffix string
7373
logger *logrus.Logger
74-
stages map[string]*stageExecutor
74+
stages map[int]*stageExecutor // Maps from stage indexes to their stageExecutors, serialized by stagesLock.
7575
store storage.Store
7676
contextDir string
7777
contextDirWritesAreDiscarded bool
@@ -115,17 +115,16 @@ type executor struct {
115115
layers bool
116116
saveStages bool
117117
stageLabels bool
118-
stageImageIDs map[string]string // Tracks image IDs for each stage (by name and position) for label references
118+
stageImageIDs map[int]string // Tracks image IDs for output of each stage (indexed by position) for label references. Serialized by stagesLock.
119119
noHostname bool
120120
noHosts bool
121121
useCache bool
122122
removeIntermediateCtrs bool
123123
forceRmIntermediateCtrs bool
124-
imageMap map[string]string // Used to map images that we create to handle the AS construct.
124+
imageMap map[int]string // Used to map from stage indexes to images that we create to be used in a later FROM...AS construct. Serialized by stagesLock.
125125
containerMap map[string]*buildah.Builder // Used to map from image names to only-created-for-the-rootfs containers.
126-
baseMap map[string]struct{} // Holds the names of every base image, as given.
127-
rootfsMap map[string]struct{} // Holds the names of every stage whose rootfs is referenced in a COPY or ADD instruction.
128-
afterDependency map[string]string // Maps stage names to their --after dependency.
126+
baseMap map[string]struct{} // Holds the set of names of every stage's base image, with ARGs resolved.
127+
rootfsMap map[int]struct{} // Holds the set of indexes for every stage whose rootfs is referenced in a COPY or ADD instruction.
129128
blobDirectory string
130129
excludes []string
131130
groupAdd []string
@@ -148,8 +147,8 @@ type executor struct {
148147
cachePushDestinationLookupReferenceFunc libimage.LookupReferenceFunc
149148
ociDecryptConfig *encconfig.DecryptConfig
150149
lastError error
151-
terminatedStage map[string]error
152-
stagesLock sync.Mutex
150+
terminatedStage map[int]error // maps from stage indexes to error results, serialized by stagesLock
151+
stagesLock sync.Mutex // serializes stages, stageImageIDs, imageMap, terminatedStage
153152
stagesSemaphore *semaphore.Weighted
154153
logRusage bool
155154
rusageLogFile io.Writer
@@ -270,7 +269,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
270269
cacheTTL: options.CacheTTL,
271270
containerSuffix: options.ContainerSuffix,
272271
logger: logger,
273-
stages: make(map[string]*stageExecutor),
272+
stages: make(map[int]*stageExecutor),
274273
store: store,
275274
contextDir: options.ContextDirectory,
276275
contextDirWritesAreDiscarded: contextWritesDiscarded,
@@ -318,16 +317,16 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
318317
layers: options.Layers,
319318
saveStages: options.SaveStages,
320319
stageLabels: options.StageLabels,
321-
stageImageIDs: make(map[string]string),
320+
stageImageIDs: make(map[int]string),
322321
noHostname: options.CommonBuildOpts.NoHostname,
323322
noHosts: options.CommonBuildOpts.NoHosts,
324323
useCache: !options.NoCache,
325324
removeIntermediateCtrs: options.RemoveIntermediateCtrs,
326325
forceRmIntermediateCtrs: options.ForceRmIntermediateCtrs,
327-
imageMap: make(map[string]string),
326+
imageMap: make(map[int]string),
328327
containerMap: make(map[string]*buildah.Builder),
329328
baseMap: make(map[string]struct{}),
330-
rootfsMap: make(map[string]struct{}),
329+
rootfsMap: make(map[int]struct{}),
331330
blobDirectory: options.BlobDirectory,
332331
unusedArgs: make(map[string]struct{}),
333332
capabilities: capabilities,
@@ -343,7 +342,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
343342
cachePushSourceLookupReferenceFunc: options.CachePushSourceLookupReferenceFunc,
344343
cachePushDestinationLookupReferenceFunc: options.CachePushDestinationLookupReferenceFunc,
345344
ociDecryptConfig: options.OciDecryptConfig,
346-
terminatedStage: make(map[string]error),
345+
terminatedStage: make(map[int]error),
347346
stagesSemaphore: options.JobSemaphore,
348347
logRusage: options.LogRusage,
349348
rusageLogFile: rusageLogFile,
@@ -442,10 +441,7 @@ func (b *executor) startStage(ctx context.Context, stage *imagebuilder.Stage, st
442441
output: output,
443442
stage: stage,
444443
}
445-
b.stages[stage.Name] = stageExec
446-
if idx := strconv.Itoa(stage.Position); idx != stage.Name {
447-
b.stages[idx] = stageExec
448-
}
444+
b.stages[stage.Position] = stageExec
449445
return stageExec
450446
}
451447

@@ -483,7 +479,7 @@ func (b *executor) stageIndex(nameOrIndex string, stages imagebuilder.Stages) (i
483479
func (b *executor) stageIndexUnlocked(nameOrIndex string, stages imagebuilder.Stages) (int, *stageExecutor) {
484480
for _, otherStage := range slices.Backward(stages) {
485481
if otherStage.Name == nameOrIndex || strconv.Itoa(otherStage.Position) == nameOrIndex {
486-
return otherStage.Position, b.stages[strconv.Itoa(otherStage.Position)]
482+
return otherStage.Position, b.stages[otherStage.Position]
487483
}
488484
}
489485
return -1, nil
@@ -505,7 +501,7 @@ func (b *executor) waitForStage(ctx context.Context, name string, stages imagebu
505501
}
506502

507503
b.stagesLock.Lock()
508-
terminationError, terminated := b.terminatedStage[name]
504+
terminationError, terminated := b.terminatedStage[otherStageIndex]
509505
b.stagesLock.Unlock()
510506

511507
if terminationError != nil {
@@ -628,7 +624,7 @@ func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageE
628624
// Skip if stage has no instructions
629625
if b.saveStages && b.stageLabels && len(stage.Node.Children) > 0 {
630626
// Wait for base stage if it references a previous stage
631-
// This ensures stageImageIDs map is populated before buildStageLabelLine reads it
627+
// This ensures that stageImageIDs has the base stage's output image's ID before buildStageLabelLine reads it
632628
if isStage, err := b.waitForStage(ctx, base, stages[:stageIndex]); isStage && err != nil {
633629
return "", nil, false, fmt.Errorf("waiting for base stage %s: %w", base, err)
634630
}
@@ -693,13 +689,10 @@ func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageE
693689
return "", nil, onlyBaseImage, err
694690
}
695691

696-
// Store image ID for this stage so subsequent stages can reference it in labels
692+
// Store image ID for this stage's result so subsequent stages can reference it in labels
697693
if imageID != "" {
698694
b.stagesLock.Lock()
699-
if stage.Name != "" {
700-
b.stageImageIDs[stage.Name] = imageID
701-
}
702-
b.stageImageIDs[strconv.Itoa(stageIndex)] = imageID
695+
b.stageImageIDs[stageIndex] = imageID
703696
b.stagesLock.Unlock()
704697
}
705698

@@ -742,17 +735,12 @@ func (b *executor) buildStageLabelLine(stage *imagebuilder.Stage, base string, s
742735
// Check if base of the stage is another (previous) stage.
743736
// If yes, base is set as image ID of this stage.
744737
// If not original base name is set (pullspec).
745-
for i, st := range stages {
746-
if st.Name == base || strconv.Itoa(st.Position) == base {
747-
b.stagesLock.Lock()
748-
if imgID, ok := b.stageImageIDs[base]; ok {
749-
base = imgID
750-
} else if imgID, ok := b.stageImageIDs[strconv.Itoa(i)]; ok {
751-
base = imgID
752-
}
753-
b.stagesLock.Unlock()
754-
break
738+
if otherStageIndex, _ := b.stageIndex(base, stages); otherStageIndex != -1 {
739+
b.stagesLock.Lock()
740+
if imgID, ok := b.stageImageIDs[otherStageIndex]; ok {
741+
base = imgID
755742
}
743+
b.stagesLock.Unlock()
756744
}
757745
labelLine += fmt.Sprintf(" %q=%q", "io.buildah.stage.base", base)
758746
return labelLine
@@ -892,7 +880,8 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
892880
// dependencyInfo is used later to work out if a particular stage is
893881
// needed by the target stage.
894882
dependencyMap := make(map[string]*stageDependencyInfo)
895-
// Initialize afterDependency map to track --after= dependency per stage
883+
// Initialize afterDependency map to track --after= dependency per stage, mapping
884+
// from a stage's index to the index of the one it's supposed to be built "after"
896885
afterDependency := make(map[int]int)
897886
// Build maps of every named base image and every referenced stage root
898887
// filesystem. Individual stages can use them to determine whether or
@@ -940,7 +929,6 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
940929
logrus.Debugf("base for stage %d: %q resolves to %q", stageIndex, base, baseWithArg)
941930
// Check if selected base is not an additional
942931
// build context and if base is a valid stage
943-
// add it to current stage's dependency tree.
944932
if _, ok := b.additionalBuildContexts[baseWithArg]; !ok {
945933
if _, ok := dependencyMap[baseWithArg]; ok {
946934
// update current stage's dependency info
@@ -955,7 +943,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
955943
// only allow one --after flag per FROM for now; nothing necessarily
956944
// semantically wrong with multiple --after, but keeping it conservative until a
957945
// use case shows up
958-
if _, exists := b.afterDependency[stage.Name]; exists {
946+
if _, exists := afterDependency[stage.Position]; exists {
959947
return "", nil, fmt.Errorf("FROM --after=%s: only one --after flag is allowed per FROM instruction", after)
960948
}
961949
builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults)
@@ -969,57 +957,57 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
969957
if err != nil {
970958
return "", nil, fmt.Errorf("while replacing arg variables with values for --after=%q: %w", after, err)
971959
}
972-
// If --after=<index> convert index to name
973-
if index, err := strconv.Atoi(afterResolved); err == nil && index >= 0 && index < stageIndex {
974-
afterResolved = stages[index].Name
975-
}
976960
depInfo, ok := dependencyMap[afterResolved]
977961
if !ok {
978962
return "", nil, fmt.Errorf("FROM --after=%s: stage %q not found", after, afterResolved)
979-
} else if depInfo.Position >= stageIndex {
963+
}
964+
if depInfo.Position >= stageIndex {
980965
return "", nil, fmt.Errorf("FROM --after=%s: cannot depend on later stage %q", after, afterResolved)
981966
}
982-
// Mark the stage as a dep so we actually build it
983-
currentStageInfo.Needs = append(currentStageInfo.Needs, afterResolved)
984-
// And mark it on the stage executor itself so it knows to wait before even pulling
967+
// Mark the stage as a dep so we don't skip
968+
// building it
969+
currentStageInfo.Needs = append(currentStageInfo.Needs, strconv.Itoa(depInfo.Position))
970+
// And mark it for the stage executor itself
971+
// so it knows to wait before even starting
985972
afterDependency[stage.Position] = depInfo.Position
986973
logrus.Debugf("stage %d: explicit dependency on %q(%d) via --after", stageIndex, afterResolved, depInfo.Position)
987974
}
988975
}
989976
case "ADD", "COPY":
990977
for _, flag := range child.Flags { // flags for this instruction
991-
if after, ok := strings.CutPrefix(flag, "--from="); ok {
978+
if copyFrom, ok := strings.CutPrefix(flag, "--from="); ok {
992979
// Populate dependency tree and check
993980
// if following ADD or COPY needs any other
994981
// stage.
995-
stageName := after
996982
builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults)
997983
headingArgs := argsMapToSlice(stage.Builder.HeadingArgs)
998984
userArgs := argsMapToSlice(stage.Builder.Args)
999985
localScopeArgs := argsMapToSlice(stageLocalScopeArgs)
1000986
// ProcessWord uses first match; put highest priority first so
1001987
// --build-arg overrides stage ARG overrides header ARG overrides builtin.
1002988
userArgs = slices.Concat(userArgs, localScopeArgs, headingArgs, builtinArgs)
1003-
baseWithArg, err := imagebuilder.ProcessWord(stageName, userArgs)
989+
copyFromWithArg, err := imagebuilder.ProcessWord(copyFrom, userArgs)
1004990
if err != nil {
1005-
return "", nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", stageName, err)
1006-
}
1007-
if baseWithArg != "" {
1008-
b.rootfsMap[baseWithArg] = struct{}{}
1009-
}
1010-
logrus.Debugf("stage %d name: %q resolves to %q", stageIndex, stageName, baseWithArg)
1011-
stageName = baseWithArg
1012-
// If --from=<index> convert index to name
1013-
if index, err := strconv.Atoi(stageName); err == nil && index >= 0 && index < stageIndex {
1014-
stageName = stages[index].Name
991+
return "", nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", copyFrom, err)
1015992
}
1016-
// Check if selected base is not an additional
1017-
// build context and if base is a valid stage
1018-
// add it to current stage's dependency tree.
1019-
if _, ok := b.additionalBuildContexts[stageName]; !ok {
1020-
if _, ok := dependencyMap[stageName]; ok {
993+
logrus.Debugf("stage %d name: %q resolves to %q", stageIndex, copyFrom, copyFromWithArg)
994+
// Check if this "from" is a stage; add it
995+
// to the current stage's dependency tree
996+
// unless it's been replaced by an
997+
// additional context.
998+
if _, ok := b.additionalBuildContexts[copyFromWithArg]; !ok {
999+
// Treat the "from" as a rootfs we need to preserve
1000+
if otherStageIndex, _ := b.stageIndex(copyFromWithArg, stages[:stageIndex]); otherStageIndex != -1 {
1001+
b.rootfsMap[otherStageIndex] = struct{}{}
10211002
// update current stage's dependency info
1022-
currentStageInfo.Needs = append(currentStageInfo.Needs, stageName)
1003+
depInfo, ok := dependencyMap[copyFromWithArg]
1004+
if !ok {
1005+
return "", nil, fmt.Errorf("COPY --from=%s: stage %q not found", copyFrom, copyFromWithArg)
1006+
}
1007+
if depInfo.Position >= stageIndex {
1008+
return "", nil, fmt.Errorf("COPY --from=%s: cannot depend on later stage %q", copyFrom, copyFromWithArg)
1009+
}
1010+
currentStageInfo.Needs = append(currentStageInfo.Needs, strconv.Itoa(otherStageIndex))
10231011
}
10241012
}
10251013
}
@@ -1041,11 +1029,9 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
10411029
// if it is using `--mount` and `from=` field is set
10421030
// and `from=` points to a stage consider it in
10431031
// dependency calculation.
1044-
if strings.HasPrefix(flag, "--mount=") && strings.Contains(flag, "from") {
1045-
mountFlags := strings.TrimPrefix(flag, "--mount=")
1046-
fields := strings.SplitSeq(mountFlags, ",")
1047-
for field := range fields {
1048-
if mountFrom, hasFrom := strings.CutPrefix(field, "from="); hasFrom {
1032+
if mountFlags, ok := strings.CutPrefix(flag, "--mount="); ok {
1033+
for field := range strings.SplitSeq(mountFlags, ",") {
1034+
if mountFrom, ok := strings.CutPrefix(field, "from="); ok {
10491035
builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults)
10501036
headingArgs := argsMapToSlice(stage.Builder.HeadingArgs)
10511037
userArgs := argsMapToSlice(stage.Builder.Args)
@@ -1057,15 +1043,15 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
10571043
if err != nil {
10581044
return "", nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", mountFrom, err)
10591045
}
1060-
// Check if this base is a stage if yes
1046+
// Check if this "from" is a stage; if yes
10611047
// add base to current stage's dependency tree
10621048
// unless it's been replaced by an additional context.
10631049
if _, ok := b.additionalBuildContexts[mountFromWithArg]; !ok {
1064-
// Treat from as a rootfs we need to preserve
1065-
b.rootfsMap[mountFromWithArg] = struct{}{}
1066-
if _, ok := dependencyMap[mountFromWithArg]; ok {
1050+
// Treat the "from" as a rootfs we need to preserve
1051+
if mountStageIndex, _ := b.stageIndex(mountFromWithArg, stages[:stageIndex]); mountStageIndex != -1 {
1052+
b.rootfsMap[mountStageIndex] = struct{}{}
10671053
// update current stage's dependency info
1068-
currentStageInfo.Needs = append(currentStageInfo.Needs, mountFromWithArg)
1054+
currentStageInfo.Needs = append(currentStageInfo.Needs, strconv.Itoa(mountStageIndex))
10691055
}
10701056
}
10711057
}
@@ -1076,10 +1062,8 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
10761062
}
10771063
node = node.Next // next line
10781064
}
1079-
// Last stage is always target stage.
1080-
// Since last/target stage is processed
1081-
// let's calculate dependency map of stages
1082-
// so we can mark stages which can be skipped.
1065+
// Last stage is always target stage. Since last/target stage is processed let's
1066+
// calculate dependency map of stages so we can mark stages which can be skipped.
10831067
if stage.Position == (len(stages) - 1) {
10841068
markDependencyStagesForTarget(dependencyMap, stage.Name)
10851069
}
@@ -1183,8 +1167,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
11831167
stage := stages[r.Index]
11841168

11851169
b.stagesLock.Lock()
1186-
b.terminatedStage[stage.Name] = r.Error
1187-
b.terminatedStage[strconv.Itoa(stage.Position)] = r.Error
1170+
b.terminatedStage[stage.Position] = r.Error
11881171

11891172
if r.Error != nil {
11901173
b.stagesLock.Unlock()
@@ -1195,7 +1178,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
11951178
// If this is an intermediate stage, make a note of the ID, so
11961179
// that we can look it up later.
11971180
if r.Index < len(stages)-1 && r.ImageID != "" {
1198-
b.imageMap[stage.Name] = r.ImageID
1181+
b.imageMap[stage.Position] = r.ImageID
11991182
// We're not populating the cache with intermediate
12001183
// images, so add this one to the list of images that
12011184
// we'll remove later.

0 commit comments

Comments
 (0)