From 39fd7524482b2f40b3c8e3df50775a7d82681c6a Mon Sep 17 00:00:00 2001 From: Timofey Kirillov Date: Wed, 10 May 2023 20:09:01 +0300 Subject: [PATCH] feat(multiarch): support platform setting per image in werf.yaml configuration Signed-off-by: Timofey Kirillov --- pkg/build/build_phase.go | 96 ++++++++++++++----------- pkg/build/conveyor.go | 78 ++++++++++---------- pkg/build/export_phase.go | 42 +++++------ pkg/build/image/conveyor.go | 3 + pkg/build/image/image_tree.go | 35 +++++++-- pkg/config/image_from_dockerfile.go | 1 + pkg/config/raw_image_from_dockerfile.go | 2 + pkg/config/raw_stapel_image.go | 2 + pkg/config/stapel_image_base.go | 1 + 9 files changed, 150 insertions(+), 110 deletions(-) diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index 71077391c9..ef5945a2e2 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -187,18 +187,41 @@ func (phase *BuildPhase) BeforeImages(ctx context.Context) error { } func (phase *BuildPhase) AfterImages(ctx context.Context) error { - targetPlatforms, err := phase.Conveyor.GetTargetPlatforms() + forcedTargetPlatforms := phase.Conveyor.GetForcedTargetPlatforms() + commonTargetPlatforms, err := phase.Conveyor.GetTargetPlatforms() if err != nil { - return fmt.Errorf("unable to get target platforms: %w", err) + return fmt.Errorf("invalid common target platforms: %w", err) } - if len(targetPlatforms) == 0 { - targetPlatforms = []string{phase.Conveyor.ContainerBackend.GetDefaultPlatform()} + if len(commonTargetPlatforms) == 0 { + commonTargetPlatforms = []string{phase.Conveyor.ContainerBackend.GetDefaultPlatform()} } for _, desc := range phase.Conveyor.imagesTree.GetImagesByName(false) { name, images := desc.Unpair() platforms := util.MapFuncToSlice(images, func(img *image.Image) string { return img.TargetPlatform }) + // TODO: this target platforms assertion could be removed in future versions and now exists only as a additional self-testing code + var targetPlatforms []string + if len(forcedTargetPlatforms) > 0 { + targetPlatforms = forcedTargetPlatforms + } else { + targetName := name + nameParts := strings.SplitN(name, "/", 3) + if len(nameParts) == 3 && nameParts[1] == "stage" { + targetName = nameParts[0] + } + + imageTargetPlatforms, err := phase.Conveyor.GetImageTargetPlatforms(targetName) + if err != nil { + return fmt.Errorf("invalid image %q target platforms: %w", name, err) + } + if len(imageTargetPlatforms) > 0 { + targetPlatforms = imageTargetPlatforms + } else { + targetPlatforms = commonTargetPlatforms + } + } + AssertAllTargetPlatformsPresent: for _, targetPlatform := range targetPlatforms { for _, platform := range platforms { @@ -426,49 +449,38 @@ func (phase *BuildPhase) publishMultiplatformImageMetadata(ctx context.Context, } func (phase *BuildPhase) createReport(ctx context.Context) error { - targetPlatforms, err := phase.Conveyor.GetTargetPlatforms() - if err != nil { - return fmt.Errorf("invalid target platforms: %w", err) - } - if len(targetPlatforms) == 0 { - targetPlatforms = []string{phase.Conveyor.ContainerBackend.GetDefaultPlatform()} - } - - for _, img := range phase.Conveyor.imagesTree.GetImages() { - if !img.IsFinal() { - continue - } + for _, desc := range phase.Conveyor.imagesTree.GetImagesByName(true) { + name, images := desc.Unpair() + targetPlatforms := util.MapFuncToSlice(images, func(img *image.Image) string { return img.TargetPlatform }) - stageImage := img.GetLastNonEmptyStage().GetStageImage().Image - desc := stageImage.GetFinalStageDescription() - if desc == nil { - desc = stageImage.GetStageDescription() - } + for _, img := range images { + stageImage := img.GetLastNonEmptyStage().GetStageImage().Image + desc := stageImage.GetFinalStageDescription() + if desc == nil { + desc = stageImage.GetStageDescription() + } - record := ReportImageRecord{ - WerfImageName: img.GetName(), - DockerRepo: desc.Info.Repository, - DockerTag: desc.Info.Tag, - DockerImageID: desc.Info.ID, - DockerImageDigest: desc.Info.RepoDigest, - DockerImageName: desc.Info.Name, - Rebuilt: img.GetRebuilt(), - } + record := ReportImageRecord{ + WerfImageName: img.GetName(), + DockerRepo: desc.Info.Repository, + DockerTag: desc.Info.Tag, + DockerImageID: desc.Info.ID, + DockerImageDigest: desc.Info.RepoDigest, + DockerImageName: desc.Info.Name, + Rebuilt: img.GetRebuilt(), + } - if os.Getenv("WERF_ENABLE_REPORT_BY_PLATFORM") == "1" { - phase.ImagesReport.SetImageByPlatformRecord(img.TargetPlatform, img.GetName(), record) - } - if len(targetPlatforms) == 1 { - phase.ImagesReport.SetImageRecord(img.Name, record) + if os.Getenv("WERF_ENABLE_REPORT_BY_PLATFORM") == "1" { + phase.ImagesReport.SetImageByPlatformRecord(img.TargetPlatform, img.GetName(), record) + } + if len(targetPlatforms) == 1 { + phase.ImagesReport.SetImageRecord(img.Name, record) + } } - } - if _, isLocal := phase.Conveyor.StorageManager.GetStagesStorage().(*storage.LocalStagesStorage); !isLocal { - if len(targetPlatforms) > 1 { - for _, img := range phase.Conveyor.imagesTree.GetMultiplatformImages() { - if !img.IsFinal() { - continue - } + if _, isLocal := phase.Conveyor.StorageManager.GetStagesStorage().(*storage.LocalStagesStorage); !isLocal { + if len(targetPlatforms) > 1 { + img := phase.Conveyor.imagesTree.GetMultiplatformImage(name) isRebuilt := false for _, pImg := range img.Images { diff --git a/pkg/build/conveyor.go b/pkg/build/conveyor.go index 6f52570ca6..e9cf292921 100644 --- a/pkg/build/conveyor.go +++ b/pkg/build/conveyor.go @@ -121,23 +121,44 @@ func NewConveyor(werfConfig *config.WerfConfig, giterminismManager giterminism_m return c } -func (c *Conveyor) GetTargetPlatforms() ([]string, error) { - if len(c.ConveyorOptions.TargetPlatforms) > 0 { - return c.ConveyorOptions.TargetPlatforms, nil - } - - for _, p := range c.werfConfig.Meta.Build.Platform { +func validatePlatforms(platforms []string) error { + for _, p := range platforms { parts := strings.Split(p, ",") if len(parts) > 1 { - return nil, fmt.Errorf("invalid platform specified %q: specify multiple platforms using yaml array", p) + return fmt.Errorf("invalid platform specified %q: specify multiple platforms using yaml array", p) } } + return nil +} - platforms, err := platformutil.NormalizeUserParams(c.werfConfig.Meta.Build.Platform) +func prepareConfigurationPlatforms(platforms []string) ([]string, error) { + if err := validatePlatforms(platforms); err != nil { + return nil, fmt.Errorf("unable to validate platforms: %w", err) + } + res, err := platformutil.NormalizeUserParams(platforms) if err != nil { - return nil, fmt.Errorf("unable to normalize platforms specified in the werf.yaml %v: %w", c.werfConfig.Meta.Build.Platform, err) + return nil, fmt.Errorf("unable to normalize platforms specified in the werf.yaml %v: %w", platforms, err) + } + return res, nil +} + +func (c *Conveyor) GetImageTargetPlatforms(targetImageName string) ([]string, error) { + if img := c.werfConfig.GetStapelImage(targetImageName); img != nil { + return prepareConfigurationPlatforms(img.Platform) + } else if img := c.werfConfig.GetArtifact(targetImageName); img != nil { + return prepareConfigurationPlatforms(img.Platform) + } else if img := c.werfConfig.GetDockerfileImage(targetImageName); img != nil { + return prepareConfigurationPlatforms(img.Platform) } - return platforms, nil + return nil, nil +} + +func (c *Conveyor) GetForcedTargetPlatforms() []string { + return c.ConveyorOptions.TargetPlatforms +} + +func (c *Conveyor) GetTargetPlatforms() ([]string, error) { + return prepareConfigurationPlatforms(c.werfConfig.Meta.Build.Platform) } func (c *Conveyor) GetServiceRWMutex(service string) *sync.RWMutex { @@ -383,41 +404,26 @@ func (c *Conveyor) FetchLastImageStage(ctx context.Context, targetPlatform, imag } func (c *Conveyor) GetImageInfoGetters(opts imagePkg.InfoGetterOptions) ([]*imagePkg.InfoGetter, error) { - targetPlatforms, err := c.GetTargetPlatforms() - if err != nil { - return nil, fmt.Errorf("unable to get target platforms: %w", err) - } - if len(targetPlatforms) == 0 { - targetPlatforms = []string{c.ContainerBackend.GetDefaultPlatform()} - } + var imagesGetters []*imagePkg.InfoGetter + for _, desc := range c.imagesTree.GetImagesByName(true) { + name, images := desc.Unpair() + platforms := util.MapFuncToSlice(images, func(img *image.Image) string { return img.TargetPlatform }) - var images []*imagePkg.InfoGetter - - if len(targetPlatforms) == 1 { - for _, img := range c.imagesTree.GetImages() { - if img.IsArtifact { - continue - } + if len(platforms) == 1 { + img := images[0] getter := c.StorageManager.GetImageInfoGetter(img.Name, img.GetLastNonEmptyStage().GetStageImage().Image.GetStageDescription(), opts) - images = append(images, getter) - } - } else { - for _, img := range c.imagesTree.GetMultiplatformImages() { - if !img.IsFinal() { - continue - } - + imagesGetters = append(imagesGetters, getter) + } else { + img := c.imagesTree.GetMultiplatformImage(name) desc := img.GetFinalStageDescription() if desc == nil { desc = img.GetStageDescription() } - getter := c.StorageManager.GetImageInfoGetter(img.Name, desc, opts) - images = append(images, getter) + imagesGetters = append(imagesGetters, getter) } } - - return images, nil + return imagesGetters, nil } func (c *Conveyor) GetExportedImages() (res []*image.Image) { diff --git a/pkg/build/export_phase.go b/pkg/build/export_phase.go index 1c44f9d966..58f96142e2 100644 --- a/pkg/build/export_phase.go +++ b/pkg/build/export_phase.go @@ -13,6 +13,7 @@ import ( build_image "github.com/werf/werf/pkg/build/image" "github.com/werf/werf/pkg/image" "github.com/werf/werf/pkg/storage" + "github.com/werf/werf/pkg/util" ) type ExportPhase struct { @@ -42,38 +43,27 @@ func (phase *ExportPhase) AfterImages(ctx context.Context) error { return nil } - targetPlatforms, err := phase.Conveyor.GetTargetPlatforms() - if err != nil { - return fmt.Errorf("unable to get target platforms: %w", err) - } - if len(targetPlatforms) == 0 { - targetPlatforms = []string{phase.Conveyor.ContainerBackend.GetDefaultPlatform()} - } + for _, desc := range phase.Conveyor.imagesTree.GetImagesByName(true) { + name, images := desc.Unpair() + if !slices.Contains(phase.ExportImageNameList, name) { + continue + } - if len(targetPlatforms) == 1 { - // single platform mode - for _, desc := range phase.Conveyor.imagesTree.GetImagesByName(true) { - _, images := desc.Unpair() + targetPlatforms := util.MapFuncToSlice(images, func(img *build_image.Image) string { return img.TargetPlatform }) + if len(targetPlatforms) == 1 { img := images[0] - if !slices.Contains(phase.ExportImageNameList, img.Name) { - continue - } if err := phase.exportImage(ctx, img); err != nil { return fmt.Errorf("unable to export image %q: %w", img.Name, err) } - } - } else { - // FIXME(multiarch): Support multiplatform manifest by pushing local images to repo first, then create manifest list. - // FIXME(multiarch): Also support multiplatform manifest in werf build command in local mode with enabled final-repo. - if _, isLocal := phase.Conveyor.StorageManager.GetStagesStorage().(*storage.LocalStagesStorage); isLocal { - return fmt.Errorf("export command is not supported in multiplatform mode") - } - - // multiplatform mode - for _, img := range phase.Conveyor.imagesTree.GetMultiplatformImages() { - if !slices.Contains(phase.ExportImageNameList, img.Name) { - continue + } else { + // FIXME(multiarch): Support multiplatform manifest by pushing local images to repo first, then create manifest list. + // FIXME(multiarch): Also support multiplatform manifest in werf build command in local mode with enabled final-repo. + if _, isLocal := phase.Conveyor.StorageManager.GetStagesStorage().(*storage.LocalStagesStorage); isLocal { + return fmt.Errorf("export command is not supported in multiplatform mode") } + + // multiplatform mode + img := phase.Conveyor.imagesTree.GetMultiplatformImage(name) if err := phase.exportMultiplatformImage(ctx, img); err != nil { return fmt.Errorf("unable to export multiplatform image %q: %w", img.Name, err) } diff --git a/pkg/build/image/conveyor.go b/pkg/build/image/conveyor.go index 73a3fb2772..4542c0846f 100644 --- a/pkg/build/image/conveyor.go +++ b/pkg/build/image/conveyor.go @@ -12,7 +12,10 @@ type Conveyor interface { GetImage(targetPlatform, name string) *Image GetOrCreateStageImage(name string, prevStageImage *stage.StageImage, stg stage.Interface, img *Image) *stage.StageImage + + GetForcedTargetPlatforms() []string GetTargetPlatforms() ([]string, error) + GetImageTargetPlatforms(imageName string) ([]string, error) IsBaseImagesRepoIdsCacheExist(key string) bool GetBaseImagesRepoIdsCache(key string) string diff --git a/pkg/build/image/image_tree.go b/pkg/build/image/image_tree.go index a8678168e4..116ca9dbdd 100644 --- a/pkg/build/image/image_tree.go +++ b/pkg/build/image/image_tree.go @@ -51,21 +51,37 @@ func (tree *ImagesTree) Calculate(ctx context.Context) error { return fmt.Errorf("unable to group werf config images by independent sets: %w", err) } - targetPlatforms, err := tree.Conveyor.GetTargetPlatforms() + forcedTargetPlatforms := tree.Conveyor.GetForcedTargetPlatforms() + commonTargetPlatforms, err := tree.Conveyor.GetTargetPlatforms() if err != nil { - return fmt.Errorf("invalid target platforms: %w", err) + return fmt.Errorf("invalid common target platforms: %w", err) } - if len(targetPlatforms) == 0 { - targetPlatforms = []string{tree.ContainerBackend.GetDefaultPlatform()} + if len(commonTargetPlatforms) == 0 { + commonTargetPlatforms = []string{tree.ContainerBackend.GetDefaultPlatform()} } commonImageOpts := tree.CommonImageOptions - commonImageOpts.ForceTargetPlatformLogging = (len(targetPlatforms) > 1) - builder := NewImagesSetsBuilder() for _, iteration := range imageConfigSets { for _, imageConfigI := range iteration { + var targetPlatforms []string + if len(forcedTargetPlatforms) > 0 { + targetPlatforms = forcedTargetPlatforms + } else { + imageTargetPlatforms, err := tree.Conveyor.GetImageTargetPlatforms(imageConfigI.GetName()) + if err != nil { + return fmt.Errorf("invalid image %q target platforms: %w", imageConfigI.GetName(), err) + } + if len(imageTargetPlatforms) > 0 { + targetPlatforms = imageTargetPlatforms + } else { + targetPlatforms = commonTargetPlatforms + } + } + + commonImageOpts.ForceTargetPlatformLogging = (len(targetPlatforms) > 1) + for _, targetPlatform := range targetPlatforms { var imageLogName string var style color.Style @@ -181,6 +197,13 @@ func (tree *ImagesTree) GetImagePlatformsByName(finalOnly bool) map[string][]str return res } +func (tree *ImagesTree) GetImagesNames() (res []string) { + for _, img := range tree.allImages { + res = util.UniqAppendString(res, img.Name) + } + return +} + func (tree *ImagesTree) GetImages() []*Image { return tree.allImages } diff --git a/pkg/config/image_from_dockerfile.go b/pkg/config/image_from_dockerfile.go index 3221a0571d..716d8b2416 100644 --- a/pkg/config/image_from_dockerfile.go +++ b/pkg/config/image_from_dockerfile.go @@ -19,6 +19,7 @@ type ImageFromDockerfile struct { SSH string Dependencies []*Dependency Staged bool + Platform []string raw *rawImageFromDockerfile } diff --git a/pkg/config/raw_image_from_dockerfile.go b/pkg/config/raw_image_from_dockerfile.go index 95143dcae4..cbd4aebcdb 100644 --- a/pkg/config/raw_image_from_dockerfile.go +++ b/pkg/config/raw_image_from_dockerfile.go @@ -20,6 +20,7 @@ type rawImageFromDockerfile struct { SSH string `yaml:"ssh,omitempty"` RawDependencies []*rawDependency `yaml:"dependencies,omitempty"` Staged bool `yaml:"staged,omitempty"` + Platform []string `yaml:"platform,omitempty"` doc *doc `yaml:"-"` // parent @@ -128,6 +129,7 @@ func (c *rawImageFromDockerfile) toImageFromDockerfileDirective(giterminismManag } image.Staged = c.Staged || util.GetBoolEnvironmentDefaultFalse("WERF_FORCE_STAGED_DOCKERFILE") + image.Platform = append([]string{}, c.Platform...) image.raw = c if err := image.validate(giterminismManager); err != nil { diff --git a/pkg/config/raw_stapel_image.go b/pkg/config/raw_stapel_image.go index c0cf16fdc5..ada189a56c 100644 --- a/pkg/config/raw_stapel_image.go +++ b/pkg/config/raw_stapel_image.go @@ -21,6 +21,7 @@ type rawStapelImage struct { RawDocker *rawDocker `yaml:"docker,omitempty"` RawImport []*rawImport `yaml:"import,omitempty"` RawDependencies []*rawDependency `yaml:"dependencies,omitempty"` + Platform []string `yaml:"platform,omitempty"` doc *doc `yaml:"-"` // parent @@ -213,6 +214,7 @@ func (c *rawStapelImage) toStapelImageBaseDirective(giterminismManager gitermini imageBase.FromArtifactName = c.FromArtifact imageBase.FromLatest = c.FromLatest imageBase.FromCacheVersion = c.FromCacheVersion + imageBase.Platform = append([]string{}, c.Platform...) for _, git := range c.RawGit { if git.gitType() == "local" { diff --git a/pkg/config/stapel_image_base.go b/pkg/config/stapel_image_base.go index c16d8905d8..37a0b71344 100644 --- a/pkg/config/stapel_image_base.go +++ b/pkg/config/stapel_image_base.go @@ -21,6 +21,7 @@ type StapelImageBase struct { Mount []*Mount Import []*Import Dependencies []*Dependency + Platform []string raw *rawStapelImage }