Skip to content

Commit

Permalink
fix(staged-dockerfile): separate FROM stage with caching in the conta…
Browse files Browse the repository at this point in the history
…iner-registry and possibility to reset by global cache version

* This also fixes panic on single FROM instruction stage.
* Added possibility to reset FROM instruction stage cache.

Signed-off-by: Timofey Kirillov <timofey.kirillov@flant.com>
  • Loading branch information
distorhead committed May 23, 2023
1 parent b983de6 commit dd3d653
Show file tree
Hide file tree
Showing 21 changed files with 327 additions and 120 deletions.
43 changes: 29 additions & 14 deletions pkg/build/build_phase.go
Expand Up @@ -465,7 +465,7 @@ func (phase *BuildPhase) createReport(ctx context.Context) error {
DockerRepo: desc.Info.Repository,
DockerTag: desc.Info.Tag,
DockerImageID: desc.Info.ID,
DockerImageDigest: desc.Info.RepoDigest,
DockerImageDigest: desc.Info.GetDigest(),
DockerImageName: desc.Info.Name,
Rebuilt: img.GetRebuilt(),
}
Expand Down Expand Up @@ -497,7 +497,7 @@ func (phase *BuildPhase) createReport(ctx context.Context) error {
DockerRepo: desc.Info.Repository,
DockerTag: desc.Info.Tag,
DockerImageID: desc.Info.ID,
DockerImageDigest: desc.Info.RepoDigest,
DockerImageDigest: desc.Info.GetDigest(),
DockerImageName: desc.Info.Name,
Rebuilt: isRebuilt,
}
Expand Down Expand Up @@ -874,13 +874,18 @@ func (phase *BuildPhase) calculateStage(ctx context.Context, img *image.Image, s
return false, nil, err
}

var opts calculateDigestOption
if img.IsDockerfileImage && img.DockerfileImageConfig.Staged {
if !stg.HasPrevStage() {
opts.BaseImage = img.GetBaseImageReference()
var opts calculateDigestOptions
// TODO: common cache version / per image cache version / fromCacheVersion goes into this
opts.CacheVersionParts = nil
opts.TargetPlatform = img.TargetPlatform

if werf.GetStagedDockerfileVersion() == werf.StagedDockerfileV1 {
if img.IsDockerfileImage && img.DockerfileImageConfig.Staged {
if !stg.HasPrevStage() {
opts.BaseImage = img.GetBaseImageReference()
}
}
}
opts.TargetPlatform = img.TargetPlatform

stageDigest, err := calculateDigest(ctx, stage.GetLegacyCompatibleStageName(stg.Name()), stageDependencies, phase.StagesIterator.PrevNonEmptyStage, phase.Conveyor, opts)
if err != nil {
Expand Down Expand Up @@ -911,7 +916,7 @@ func (phase *BuildPhase) calculateStage(ctx context.Context, img *image.Image, s
}
}

stageContentSig, err := calculateDigest(ctx, fmt.Sprintf("%s-content", stg.Name()), "", stg, phase.Conveyor, calculateDigestOption{TargetPlatform: img.TargetPlatform})
stageContentSig, err := calculateDigest(ctx, fmt.Sprintf("%s-content", stg.Name()), "", stg, phase.Conveyor, calculateDigestOptions{TargetPlatform: img.TargetPlatform})
if err != nil {
return false, phase.Conveyor.GetStageDigestMutex(stg.GetDigest()).Unlock, fmt.Errorf("unable to calculate stage %s content digest: %w", stg.Name(), err)
}
Expand Down Expand Up @@ -1032,8 +1037,8 @@ func (phase *BuildPhase) buildStage(ctx context.Context, img *image.Image, stg s
options.Style(style.Highlight())
}).
DoError(func() (err error) {
if err := stg.PreRunHook(ctx, phase.Conveyor); err != nil {
return fmt.Errorf("%s preRunHook failed: %w", stg.LogDetailedName(), err)
if err := stg.PreRun(ctx, phase.Conveyor); err != nil {
return fmt.Errorf("%s preRun failed: %w", stg.LogDetailedName(), err)
}

return phase.atomicBuildStageImage(ctx, img, stg)
Expand Down Expand Up @@ -1168,12 +1173,13 @@ func introspectStage(ctx context.Context, s stage.Interface) error {
})
}

type calculateDigestOption struct {
BaseImage string
TargetPlatform string
type calculateDigestOptions struct {
TargetPlatform string
CacheVersionParts []string
BaseImage string // TODO(staged-dockerfile): legacy compatibility field
}

func calculateDigest(ctx context.Context, stageName, stageDependencies string, prevNonEmptyStage stage.Interface, conveyor *Conveyor, opts calculateDigestOption) (string, error) {
func calculateDigest(ctx context.Context, stageName, stageDependencies string, prevNonEmptyStage stage.Interface, conveyor *Conveyor, opts calculateDigestOptions) (string, error) {
var checksumArgs []string
var checksumArgsNames []string

Expand Down Expand Up @@ -1203,6 +1209,15 @@ func calculateDigest(ctx context.Context, stageName, stageDependencies string, p
)
}

if len(opts.CacheVersionParts) > 0 {
for i, cacheVersion := range opts.CacheVersionParts {
name := fmt.Sprintf("CacheVersion%d", i)
checksumArgsNames = append(checksumArgsNames, name)
checksumArgs = append(checksumArgs, name, cacheVersion)
}
}

// TODO(staged-dockerfile): this is legacy digest part used for StagedDockerfileV1
if opts.BaseImage != "" {
checksumArgs = append(checksumArgs, opts.BaseImage)
checksumArgsNames = append(checksumArgsNames, "BaseImage")
Expand Down
2 changes: 1 addition & 1 deletion pkg/build/conveyor.go
Expand Up @@ -852,7 +852,7 @@ func (c *Conveyor) GetImageIDForLastImageStage(targetPlatform, imageName string)
}

func (c *Conveyor) GetImageDigestForLastImageStage(targetPlatform, imageName string) string {
return c.GetImage(targetPlatform, imageName).GetLastNonEmptyStage().GetStageImage().Image.GetStageDescription().Info.RepoDigest
return c.GetImage(targetPlatform, imageName).GetLastNonEmptyStage().GetStageImage().Image.GetStageDescription().Info.GetDigest()
}

func (c *Conveyor) GetImageIDForImageStage(targetPlatform, imageName, stageName string) string {
Expand Down
71 changes: 45 additions & 26 deletions pkg/build/image/dockerfile.go
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/werf/werf/pkg/giterminism_manager"
"github.com/werf/werf/pkg/path_matcher"
"github.com/werf/werf/pkg/util"
"github.com/werf/werf/pkg/werf"
)

func MapDockerfileConfigToImagesSets(ctx context.Context, dockerfileImageConfig *config.ImageFromDockerfile, targetPlatform string, opts CommonImageOptions) (ImagesSets, error) {
Expand Down Expand Up @@ -152,54 +153,70 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
}
}

for ind, instr := range stg.Instructions {
stageLogName := fmt.Sprintf("%s%d", strings.ToUpper(instr.GetInstructionData().Name()), ind+1)
commonBaseStageOptions := &stage.BaseStageOptions{
TargetPlatform: img.TargetPlatform,
ImageName: img.Name,
ImageTmpDir: img.TmpDir,
ContainerWerfDir: img.ContainerWerfDir,
ProjectName: opts.ProjectName,
}

var instrNum int

if werf.GetStagedDockerfileVersion() == werf.StagedDockerfileV2 {
baseStageOptions := *commonBaseStageOptions
baseStageOptions.LogName = "FROM1"
img.stages = append(img.stages, stage_instruction.NewFrom(
img.GetBaseImageReference(), img.GetBaseImageRepoDigest(),
&baseStageOptions,
))
instrNum = 1
} else {
instrNum = 0
}

for _, instr := range stg.Instructions {
stageLogName := fmt.Sprintf("%s%d", strings.ToUpper(instr.GetInstructionData().Name()), instrNum+1)
isFirstStage := (len(img.stages) == 0)
baseStageOptions := &stage.BaseStageOptions{
TargetPlatform: img.TargetPlatform,
ImageName: img.Name,
LogName: stageLogName,
ImageTmpDir: img.TmpDir,
ContainerWerfDir: img.ContainerWerfDir,
ProjectName: opts.ProjectName,
}
baseStageOptions := *commonBaseStageOptions
baseStageOptions.LogName = stageLogName

var stg stage.Interface
switch typedInstr := any(instr).(type) {
case *dockerfile.DockerfileStageInstruction[*instructions.ArgCommand]:
continue
case *dockerfile.DockerfileStageInstruction[*instructions.AddCommand]:
stg = stage_instruction.NewAdd(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewAdd(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.CmdCommand]:
stg = stage_instruction.NewCmd(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewCmd(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.CopyCommand]:
stg = stage_instruction.NewCopy(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewCopy(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.EntrypointCommand]:
stg = stage_instruction.NewEntrypoint(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewEntrypoint(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.EnvCommand]:
stg = stage_instruction.NewEnv(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewEnv(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.ExposeCommand]:
stg = stage_instruction.NewExpose(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewExpose(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.HealthCheckCommand]:
stg = stage_instruction.NewHealthcheck(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewHealthcheck(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.LabelCommand]:
stg = stage_instruction.NewLabel(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewLabel(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.MaintainerCommand]:
stg = stage_instruction.NewMaintainer(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewMaintainer(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.OnbuildCommand]:
stg = stage_instruction.NewOnBuild(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewOnBuild(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.RunCommand]:
stg = stage_instruction.NewRun(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewRun(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.ShellCommand]:
stg = stage_instruction.NewShell(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewShell(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.StopSignalCommand]:
stg = stage_instruction.NewStopSignal(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewStopSignal(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.UserCommand]:
stg = stage_instruction.NewUser(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewUser(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.VolumeCommand]:
stg = stage_instruction.NewVolume(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewVolume(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.WorkdirCommand]:
stg = stage_instruction.NewWorkdir(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewWorkdir(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, &baseStageOptions)
default:
panic(fmt.Sprintf("unsupported instruction type %#v", instr))
}
Expand All @@ -209,6 +226,8 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
for _, dep := range instr.GetDependenciesByStageRef() {
appendQueue(dep.GetWerfImageName(), dep, item.Level+1)
}

instrNum++
}

appendImageToCurrentSet(img)
Expand Down
51 changes: 38 additions & 13 deletions pkg/build/image/image.go
Expand Up @@ -80,7 +80,7 @@ func NewImage(ctx context.Context, targetPlatform, name string, baseImageType Ba
}

if opts.FetchLatestBaseImage {
if _, err := i.getFromBaseImageIdFromRegistry(ctx, i.baseImageReference); err != nil {
if err := i.setupBaseImageRepoDigest(ctx, i.baseImageReference); err != nil {
return nil, fmt.Errorf("error fetching base image id from registry: %w", err)
}
}
Expand Down Expand Up @@ -108,7 +108,10 @@ type Image struct {
baseImageName string
dockerfileExpanderFactory dockerfile.ExpanderFactory

baseImageRepoId string
// NOTICE: baseImageRepoId is a legacy field, better use Digest instead everywhere
baseImageRepoId string
baseImageRepoDigest string

baseStageImage *stage.StageImage
stageAsBaseImage stage.Interface
}
Expand Down Expand Up @@ -233,6 +236,8 @@ func isUnsupportedMediaTypeError(err error) bool {
}

func (i *Image) SetupBaseImage(ctx context.Context, storageManager manager.StorageManagerInterface, storageOpts manager.StorageOptions) error {
logboek.Context(ctx).Debug().LogF(" -- SetupBaseImage for %q\n", i.Name)

switch i.baseImageType {
case StageAsBaseImage:
i.stageAsBaseImage = i.Conveyor.GetImage(i.TargetPlatform, i.baseImageName).GetLastNonEmptyStage()
Expand Down Expand Up @@ -318,7 +323,13 @@ func (i *Image) GetBaseImageReference() string {
return i.baseImageReference
}

func (i *Image) GetBaseImageRepoDigest() string {
return i.baseImageRepoDigest
}

func (i *Image) FetchBaseImage(ctx context.Context) error {
logboek.Context(ctx).Debug().LogF(" -- FetchBaseImage for %q\n", i.Name)

switch i.baseImageType {
case ImageFromRegistryAsBaseImage:
if i.baseStageImage.Image.Name() == "scratch" {
Expand All @@ -330,20 +341,24 @@ func (i *Image) FetchBaseImage(ctx context.Context) error {
if info, err := i.ContainerBackend.GetImageInfo(ctx, i.baseStageImage.Image.Name(), container_backend.GetImageInfoOpts{}); err != nil {
return fmt.Errorf("unable to inspect local image %s: %w", i.baseStageImage.Image.Name(), err)
} else if info != nil {
logboek.Context(ctx).Debug().LogF("GetImageInfo of %q -> %#v\n", i.baseStageImage.Image.Name(), info)

// TODO: do not use container_backend.LegacyStageImage for base image
i.baseStageImage.Image.SetStageDescription(&image.StageDescription{
StageID: nil, // this is not a stage actually, TODO
Info: info,
})

baseImageRepoId, err := i.getFromBaseImageIdFromRegistry(ctx, i.baseStageImage.Image.Name())
if baseImageRepoId == info.ID || (err != nil && !isUnsupportedMediaTypeError(err)) {
err = i.setupBaseImageRepoDigest(ctx, i.baseStageImage.Image.Name())
if (i.baseImageRepoDigest != "" && i.baseImageRepoDigest == info.RepoDigest) || (err != nil && !isUnsupportedMediaTypeError(err)) {
if err != nil {
logboek.Context(ctx).Warn().LogF("WARNING: cannot get base image id (%s): %s\n", i.baseStageImage.Image.Name(), err)
logboek.Context(ctx).Warn().LogF("WARNING: using existing image %s without pull\n", i.baseStageImage.Image.Name())
logboek.Context(ctx).Warn().LogOptionalLn()
} else {
logboek.Context(ctx).Info().LogF("No pull needed for base image %s of image %q: image by digest %s is up to date\n", i.baseImageReference, i.Name, i.baseImageRepoDigest)
}

// No image pull
return nil
}
}
Expand Down Expand Up @@ -384,18 +399,27 @@ func (i *Image) FetchBaseImage(ctx context.Context) error {
}
}

func (i *Image) getFromBaseImageIdFromRegistry(ctx context.Context, reference string) (string, error) {
func packRepoIDAndDigest(repoID, digest string) string {
return fmt.Sprintf("%s/%s", repoID, digest)
}

func unpackRepoIDAndDigest(packed string) (string, string) {
parts := strings.SplitN(packed, "/", 2)
return parts[0], parts[1]
}

func (i *Image) setupBaseImageRepoDigest(ctx context.Context, reference string) error {
i.Conveyor.GetServiceRWMutex("baseImagesRepoIdsCache" + reference).Lock()
defer i.Conveyor.GetServiceRWMutex("baseImagesRepoIdsCache" + reference).Unlock()

switch {
case i.baseImageRepoId != "":
return i.baseImageRepoId, nil
return nil
case i.Conveyor.IsBaseImagesRepoIdsCacheExist(reference):
i.baseImageRepoId = i.Conveyor.GetBaseImagesRepoIdsCache(reference)
return i.baseImageRepoId, nil
i.baseImageRepoId, i.baseImageRepoDigest = unpackRepoIDAndDigest(i.Conveyor.GetBaseImagesRepoIdsCache(reference))
return nil
case i.Conveyor.IsBaseImagesRepoErrCacheExist(reference):
return "", i.Conveyor.GetBaseImagesRepoErrCache(reference)
return i.Conveyor.GetBaseImagesRepoErrCache(reference)
}

var fetchedBaseRepoImage *image.Info
Expand All @@ -410,13 +434,14 @@ func (i *Image) getFromBaseImageIdFromRegistry(ctx context.Context, reference st

return nil
}); err != nil {
return "", err
return err
}

i.baseImageRepoId = fetchedBaseRepoImage.ID
i.Conveyor.SetBaseImagesRepoIdsCache(reference, i.baseImageRepoId)
i.baseImageRepoDigest = fetchedBaseRepoImage.RepoDigest
i.Conveyor.SetBaseImagesRepoIdsCache(reference, packRepoIDAndDigest(i.baseImageRepoId, i.baseImageRepoDigest))

return i.baseImageRepoId, nil
return nil
}

func EnvToMap(env []string) map[string]string {
Expand Down
2 changes: 1 addition & 1 deletion pkg/build/stage/base.go
Expand Up @@ -299,7 +299,7 @@ func (s *BaseStage) PrepareImage(ctx context.Context, c Conveyor, cb container_b
return nil
}

func (s *BaseStage) PreRunHook(_ context.Context, _ Conveyor) error {
func (s *BaseStage) PreRun(_ context.Context, _ Conveyor) error {
return nil
}

Expand Down

0 comments on commit dd3d653

Please sign in to comment.