diff --git a/pkg/config/dependency.go b/pkg/config/dependency.go index 32839366dc..d46ccff8f9 100644 --- a/pkg/config/dependency.go +++ b/pkg/config/dependency.go @@ -1,8 +1,58 @@ package config +import ( + "fmt" + "strings" + + "github.com/werf/werf/pkg/util" +) + type Dependency struct { ImageName string + Before string + After string Imports []*DependencyImport - // TODO: raw *rawDependencies + raw *rawDependency +} + +func (d *Dependency) validate() error { + switch { + case d.ImageName == "": + return newDetailedConfigError("image name is not specified for dependency", d.raw, d.raw.doc()) + case d.Before == "" && d.After == "": + return newDetailedConfigError("dependency stage is not specified with `before: install|setup` or `after: install|setup`", d.raw, d.raw.doc()) + case d.Before != "" && d.After != "": + return newDetailedConfigError("only one dependency stage can be specified with `before: install|setup` or `after: install|setup`, but not both", d.raw, d.raw.doc()) + case d.Before != "" && d.Before != "install" && d.Before != "setup": + return newDetailedConfigError(fmt.Sprintf("invalid dependency stage `before: %s`: expected install or setup!", d.Before), d.raw, d.raw.doc()) + case d.After != "" && d.After != "install" && d.After != "setup": + return newDetailedConfigError(fmt.Sprintf("invalid dependency stage `after: %s`: expected install or setup!", d.After), d.raw, d.raw.doc()) + } + + var targetEnvs, targetBuildArgs []string + for _, depImport := range d.Imports { + if depImport.TargetEnv != "" { + targetEnvs = append(targetEnvs, depImport.TargetEnv) + } + if depImport.TargetBuildArg != "" { + targetBuildArgs = append(targetBuildArgs, depImport.TargetBuildArg) + } + } + + if len(targetEnvs) > 0 { + duplicatedTargetEnvs := util.FindDuplicatedStrings(targetEnvs) + if len(duplicatedTargetEnvs) > 0 { + return newDetailedConfigError(fmt.Sprintf("each targetEnv for dependency import should be unique, but found duplicates for: %s", strings.Join(duplicatedTargetEnvs, ", ")), d.raw, d.raw.doc()) + } + } + + if len(targetBuildArgs) > 0 { + duplicatedTargetBuildArgs := util.FindDuplicatedStrings(targetBuildArgs) + if len(duplicatedTargetBuildArgs) > 0 { + return newDetailedConfigError(fmt.Sprintf("each targetBuildArg for dependency import should be unique, but found duplicates for: %s", strings.Join(duplicatedTargetBuildArgs, ", ")), d.raw, d.raw.doc()) + } + } + + return nil } diff --git a/pkg/config/dependency_import.go b/pkg/config/dependency_import.go index 62154b3db1..cd10f8f647 100644 --- a/pkg/config/dependency_import.go +++ b/pkg/config/dependency_import.go @@ -10,21 +10,33 @@ type DependencyImport struct { TargetBuildArg string TargetEnv string - // TODO: raw *rawDependencyImport + raw *rawDependencyImport } -func (i *DependencyImport) validate(img ImageInterface) error { - switch { - case img.IsStapel() && i.TargetBuildArg != "": - return newDetailedConfigError("`targetBuildArg cannot be used in the stapel image", nil, nil) // TODO: raw - case !img.IsStapel() && i.TargetEnv != "": - return newDetailedConfigError("`targetEnv cannot be used in the dockerfile image", nil, nil) // TODO: raw - } - +func (i *DependencyImport) validate() error { switch i.Type { case ImageNameImport, ImageTagImport, ImageRepoImport: default: - return newDetailedConfigError(fmt.Sprintf("invalid `type: %s` for dependency import, expected one of: %s", i.Type, strings.Join([]string{string(ImageNameImport), string(ImageTagImport), string(ImageRepoImport)}, ", ")), nil, nil) // TODO: raw + return newDetailedConfigError(fmt.Sprintf("invalid `type: %s` for dependency import, expected one of: %s", i.Type, strings.Join([]string{string(ImageNameImport), string(ImageTagImport), string(ImageRepoImport)}, ", ")), i.raw, i.raw.rawDependency.doc()) + } + + switch imgType := i.raw.rawDependency.imageType(); imgType { + case dependencyImageTypeStapel: + switch { + case i.TargetEnv == "": + return newDetailedConfigError("targetEnv directive cannot be empty for a Stapel image", i.raw, i.raw.rawDependency.doc()) + case i.TargetBuildArg != "": + return newDetailedConfigError("targetBuildArg directive cannot be used for a Stapel image", i.raw, i.raw.rawDependency.doc()) + } + case dependencyImageTypeDockerfile: + switch { + case i.TargetBuildArg == "": + return newDetailedConfigError("targetBuildArg directive cannot be empty for a Dockerfile image", i.raw, i.raw.rawDependency.doc()) + case i.TargetEnv != "": + return newDetailedConfigError("targetEnv directive cannot be used for a Dockerfile image", i.raw, i.raw.rawDependency.doc()) + } + default: + panic(fmt.Sprintf("unexpected dependencyImageType: %s", imgType)) } return nil diff --git a/pkg/config/image_from_dockerfile.go b/pkg/config/image_from_dockerfile.go index c0ab20968e..bddc85cbe3 100644 --- a/pkg/config/image_from_dockerfile.go +++ b/pkg/config/image_from_dockerfile.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "path/filepath" "github.com/werf/werf/pkg/giterminism_manager" @@ -37,6 +38,16 @@ func (c *ImageFromDockerfile) validate(giterminismManager giterminism_manager.In } } + if len(c.Args) > 0 { + for _, dep := range c.Dependencies { + for _, depImport := range dep.Imports { + if _, ok := c.Args[depImport.TargetBuildArg]; ok { + return newDetailedConfigError(fmt.Sprintf("dockerfile `args:` build arg %q already defined in dependency import `targetBuildArg:` directive. This is not allowed, avoid duplicated build args!", depImport.TargetBuildArg), nil, c.raw.doc) + } + } + } + } + return nil } diff --git a/pkg/config/parser.go b/pkg/config/parser.go index 3fbdb9adfd..a04f0d1cf7 100644 --- a/pkg/config/parser.go +++ b/pkg/config/parser.go @@ -531,6 +531,10 @@ func prepareWerfConfig(giterminismManager giterminism_manager.Interface, rawImag return nil, err } + if err := werfConfig.validateDependencies(); err != nil { + return nil, err + } + return werfConfig, nil } diff --git a/pkg/config/raw_dependency.go b/pkg/config/raw_dependency.go new file mode 100644 index 0000000000..35dadbad85 --- /dev/null +++ b/pkg/config/raw_dependency.go @@ -0,0 +1,92 @@ +package config + +type dependencyImageType string + +var ( + dependencyImageTypeUnknown dependencyImageType = "" + dependencyImageTypeStapel dependencyImageType = "stapel" + dependencyImageTypeDockerfile dependencyImageType = "dockerfile" +) + +type rawDependency struct { + Image string `yaml:"image,omitempty"` + Before string `yaml:"before,omitempty"` + After string `yaml:"after,omitempty"` + Imports []*rawDependencyImport `yaml:"imports,omitempty"` + + rawStapelImage *rawStapelImage `yaml:"-"` // possible parent + rawImageFromDockerfile *rawImageFromDockerfile `yaml:"-"` // possible parent + + UnsupportedAttributes map[string]interface{} `yaml:",inline"` +} + +func (d *rawDependency) doc() *doc { + var document *doc + switch d.imageType() { + case dependencyImageTypeStapel: + document = d.rawStapelImage.doc + case dependencyImageTypeDockerfile: + document = d.rawImageFromDockerfile.doc + } + + return document +} + +func (d *rawDependency) UnmarshalYAML(unmarshal func(interface{}) error) error { + switch parent := parentStack.Peek().(type) { + case *rawStapelImage: + d.rawStapelImage = parent + case *rawImageFromDockerfile: + d.rawImageFromDockerfile = parent + } + + parentStack.Push(d) + type plain rawDependency + err := unmarshal((*plain)(d)) + parentStack.Pop() + if err != nil { + return err + } + + if err := checkOverflow(d.UnsupportedAttributes, d, d.doc()); err != nil { + return err + } + + return nil +} + +func (d *rawDependency) toDirective() (*Dependency, error) { + dependency := &Dependency{ + ImageName: d.Image, + Before: d.Before, + After: d.After, + raw: d, + } + + for _, rawDepImport := range d.Imports { + depImport, err := rawDepImport.toDirective() + if err != nil { + return nil, err + } + + dependency.Imports = append(dependency.Imports, depImport) + } + + if err := dependency.validate(); err != nil { + return nil, err + } + + return dependency, nil +} + +func (d *rawDependency) imageType() dependencyImageType { + if d.rawStapelImage != nil { + return dependencyImageTypeStapel + } + + if d.rawImageFromDockerfile != nil { + return dependencyImageTypeDockerfile + } + + return dependencyImageTypeUnknown +} diff --git a/pkg/config/raw_dependency_import.go b/pkg/config/raw_dependency_import.go new file mode 100644 index 0000000000..cad4f428f4 --- /dev/null +++ b/pkg/config/raw_dependency_import.go @@ -0,0 +1,43 @@ +package config + +type rawDependencyImport struct { + Type string `yaml:"type,omitempty"` + TargetBuildArg string `yaml:"targetBuildArg,omitempty"` + TargetEnv string `yaml:"targetEnv,omitempty"` + + rawDependency *rawDependency `yaml:"-"` // parent + + UnsupportedAttributes map[string]interface{} `yaml:",inline"` +} + +func (i *rawDependencyImport) UnmarshalYAML(unmarshal func(interface{}) error) error { + if parent, ok := parentStack.Peek().(*rawDependency); ok { + i.rawDependency = parent + } + + type plain rawDependencyImport + if err := unmarshal((*plain)(i)); err != nil { + return err + } + + if err := checkOverflow(i.UnsupportedAttributes, i, i.rawDependency.doc()); err != nil { + return err + } + + return nil +} + +func (i *rawDependencyImport) toDirective() (*DependencyImport, error) { + depImport := &DependencyImport{ + Type: DependencyImportType(i.Type), + TargetBuildArg: i.TargetBuildArg, + TargetEnv: i.TargetEnv, + raw: i, + } + + if err := depImport.validate(); err != nil { + return nil, err + } + + return depImport, nil +} diff --git a/pkg/config/raw_image_from_dockerfile.go b/pkg/config/raw_image_from_dockerfile.go index e9951e4d2e..d6efe60a2c 100644 --- a/pkg/config/raw_image_from_dockerfile.go +++ b/pkg/config/raw_image_from_dockerfile.go @@ -17,6 +17,7 @@ type rawImageFromDockerfile struct { AddHost interface{} `yaml:"addHost,omitempty"` Network string `yaml:"network,omitempty"` SSH string `yaml:"ssh,omitempty"` + RawDependencies []*rawDependency `yaml:"dependencies,omitempty"` doc *doc `yaml:"-"` // parent @@ -115,6 +116,15 @@ func (c *rawImageFromDockerfile) toImageFromDockerfileDirective(giterminismManag image.Network = c.Network image.SSH = c.SSH + for _, rawDep := range c.RawDependencies { + dependencyDirective, err := rawDep.toDirective() + if err != nil { + return nil, err + } + + image.Dependencies = append(image.Dependencies, dependencyDirective) + } + 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 9dae62b614..925313efd8 100644 --- a/pkg/config/raw_stapel_image.go +++ b/pkg/config/raw_stapel_image.go @@ -7,19 +7,20 @@ import ( ) type rawStapelImage struct { - Images []string `yaml:"-"` - Artifact string `yaml:"artifact,omitempty"` - From string `yaml:"from,omitempty"` - FromLatest bool `yaml:"fromLatest,omitempty"` - FromCacheVersion string `yaml:"fromCacheVersion,omitempty"` - FromImage string `yaml:"fromImage,omitempty"` - FromArtifact string `yaml:"fromArtifact,omitempty"` - RawGit []*rawGit `yaml:"git,omitempty"` - RawShell *rawShell `yaml:"shell,omitempty"` - RawAnsible *rawAnsible `yaml:"ansible,omitempty"` - RawMount []*rawMount `yaml:"mount,omitempty"` - RawDocker *rawDocker `yaml:"docker,omitempty"` - RawImport []*rawImport `yaml:"import,omitempty"` + Images []string `yaml:"-"` + Artifact string `yaml:"artifact,omitempty"` + From string `yaml:"from,omitempty"` + FromLatest bool `yaml:"fromLatest,omitempty"` + FromCacheVersion string `yaml:"fromCacheVersion,omitempty"` + FromImage string `yaml:"fromImage,omitempty"` + FromArtifact string `yaml:"fromArtifact,omitempty"` + RawGit []*rawGit `yaml:"git,omitempty"` + RawShell *rawShell `yaml:"shell,omitempty"` + RawAnsible *rawAnsible `yaml:"ansible,omitempty"` + RawMount []*rawMount `yaml:"mount,omitempty"` + RawDocker *rawDocker `yaml:"docker,omitempty"` + RawImport []*rawImport `yaml:"import,omitempty"` + RawDependencies []*rawDependency `yaml:"dependencies,omitempty"` doc *doc `yaml:"-"` // parent @@ -113,7 +114,7 @@ func (c *rawStapelImage) toStapelImageArtifactDirectives(giterminismManager gite imageArtifact := &StapelImageArtifact{} var err error - if imageArtifact.StapelImageBase, err = c.toStapelImageBaseDirective(giterminismManager, c.Artifact); err != nil { + if imageArtifact.StapelImageBase, err = c.toStapelImageBaseDirective(giterminismManager, c.Artifact, true); err != nil { return nil, err } @@ -127,7 +128,7 @@ func (c *rawStapelImage) toStapelImageArtifactDirectives(giterminismManager gite func (c *rawStapelImage) toStapelImageDirective(giterminismManager giterminism_manager.Interface, name string) (*StapelImage, error) { image := &StapelImage{} - if imageBase, err := c.toStapelImageBaseDirective(giterminismManager, name); err != nil { + if imageBase, err := c.toStapelImageBaseDirective(giterminismManager, name, false); err != nil { return nil, err } else { image.StapelImageBase = imageBase @@ -202,7 +203,7 @@ func (c *rawStapelImage) validateStapelImageArtifactDirective(imageArtifact *Sta return nil } -func (c *rawStapelImage) toStapelImageBaseDirective(giterminismManager giterminism_manager.Interface, name string) (imageBase *StapelImageBase, err error) { +func (c *rawStapelImage) toStapelImageBaseDirective(giterminismManager giterminism_manager.Interface, name string, isArtifact bool) (imageBase *StapelImageBase, err error) { if imageBase, err = c.toBaseStapelImageBaseDirective(giterminismManager, name); err != nil { return nil, err } @@ -253,6 +254,19 @@ func (c *rawStapelImage) toStapelImageBaseDirective(giterminismManager gitermini } } + if isArtifact && len(c.RawDependencies) > 0 { + return nil, newDetailedConfigError(fmt.Sprintf("dependencies directive is specified for %q artifact, but dependencies are not supported for artifacts!", name), nil, c.doc) + } + + for _, rawDep := range c.RawDependencies { + dependencyDirective, err := rawDep.toDirective() + if err != nil { + return nil, err + } + + imageBase.Dependencies = append(imageBase.Dependencies, dependencyDirective) + } + if err := c.validateStapelImageBaseDirective(giterminismManager, imageBase); err != nil { return nil, err } diff --git a/pkg/config/werf.go b/pkg/config/werf.go index b420d0ee14..77b9dc01de 100644 --- a/pkg/config/werf.go +++ b/pkg/config/werf.go @@ -215,6 +215,42 @@ func (c *WerfConfig) validateImportImage(i *Import) error { return nil } +func (c *WerfConfig) validateDependencies() error { + if err := c.validateDependenciesImages(); err != nil { + return err + } + + return nil +} + +func (c *WerfConfig) validateDependenciesImages() error { + for _, dfImage := range c.ImagesFromDockerfile { + for _, imgDep := range dfImage.Dependencies { + if imgDep.ImageName == dfImage.Name { + return newDetailedConfigError(fmt.Sprintf("image can't depend on itself: `%s`!", imgDep.ImageName), imgDep.raw, dfImage.raw.doc) + } + + if !c.HasImage(imgDep.ImageName) { + return newDetailedConfigError(fmt.Sprintf("no such image: `%s`!", imgDep.ImageName), imgDep.raw, dfImage.raw.doc) + } + } + } + + for _, stapelImage := range c.StapelImages { + for _, imgDep := range stapelImage.Dependencies { + if imgDep.ImageName == stapelImage.Name { + return newDetailedConfigError(fmt.Sprintf("image can't depend on itself: `%s`!", imgDep.ImageName), imgDep.raw, stapelImage.raw.doc) + } + + if !c.HasImage(imgDep.ImageName) { + return newDetailedConfigError(fmt.Sprintf("no such image: `%s`!", imgDep.ImageName), imgDep.raw, stapelImage.raw.doc) + } + } + } + + return nil +} + func (c *WerfConfig) validateImagesFrom() error { for _, image := range c.StapelImages { if err := c.validateImageFrom(image.StapelImageBase); err != nil { diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 349f11c682..221d64465b 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -1,5 +1,7 @@ package util +import "sort" + func UniqStrings(arr []string) []string { res := []string{} @@ -82,3 +84,24 @@ func Reverse(s string) string { } return string(r) } + +func FindDuplicatedStrings(elems []string) []string { + if len(elems) <= 1 { + return []string{} + } + + sort.Strings(elems) + + var duplicates []string + for i, elem := range elems { + if i == 0 { + continue + } + + if elem == elems[i-1] { + duplicates = append(duplicates, elem) + } + } + + return duplicates +}