Skip to content

Commit

Permalink
feat(multiarch): support platform setting per image in werf.yaml conf…
Browse files Browse the repository at this point in the history
…iguration

Signed-off-by: Timofey Kirillov <timofey.kirillov@flant.com>
  • Loading branch information
distorhead committed May 11, 2023
1 parent e9c5727 commit 39fd752
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 110 deletions.
96 changes: 54 additions & 42 deletions pkg/build/build_phase.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
78 changes: 42 additions & 36 deletions pkg/build/conveyor.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
42 changes: 16 additions & 26 deletions pkg/build/export_phase.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/build/image/conveyor.go
Expand Up @@ -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
Expand Down
35 changes: 29 additions & 6 deletions pkg/build/image/image_tree.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions pkg/config/image_from_dockerfile.go
Expand Up @@ -19,6 +19,7 @@ type ImageFromDockerfile struct {
SSH string
Dependencies []*Dependency
Staged bool
Platform []string

raw *rawImageFromDockerfile
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/raw_image_from_dockerfile.go
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 39fd752

Please sign in to comment.