diff --git a/README.md b/README.md index ae391f5f..cd99f965 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ podman run --rm -it \ volume to make the build artifacts available after the container completes. In this example, a directory named `_build` will be created in the image configuration directory and will persist after EIB finishes. This directory will contain subdirectories storing the respective artifacts of the different builds. +* `-validate` - If specified, the specified image definition and configuration directory will be checked to ensure + the build can proceed, however the image will not actually be built. ## Testing Images diff --git a/cmd/eib/main.go b/cmd/eib/main.go index f4f359c2..394cd8ac 100644 --- a/cmd/eib/main.go +++ b/cmd/eib/main.go @@ -6,10 +6,13 @@ import ( "log" "os" "path/filepath" + "slices" + "strings" "github.com/suse-edge/edge-image-builder/pkg/build" "github.com/suse-edge/edge-image-builder/pkg/combustion" "github.com/suse-edge/edge-image-builder/pkg/image" + "github.com/suse-edge/edge-image-builder/pkg/image/validation" "github.com/suse-edge/edge-image-builder/pkg/kubernetes" audit "github.com/suse-edge/edge-image-builder/pkg/log" "github.com/suse-edge/edge-image-builder/pkg/network" @@ -24,6 +27,7 @@ const ( argConfigFile = "config-file" argConfigDir = "config-dir" argBuildDir = "build-dir" + argValidate = "validate" ) func processArgs() (*image.Context, error) { @@ -31,11 +35,13 @@ func processArgs() (*image.Context, error) { configFile string configDir string rootBuildDir string + validate bool ) flag.StringVar(&configFile, argConfigFile, "", "name of the image configuration file") flag.StringVar(&configDir, argConfigDir, "", "full path to the image configuration directory") flag.StringVar(&rootBuildDir, argBuildDir, "", "full path to the directory to store build artifacts") + flag.BoolVar(&validate, argValidate, false, "if specified, the image definition will be validated but not built") flag.Parse() imageDefinition, err := parseImageDefinition(configFile, configDir) @@ -66,6 +72,40 @@ func processArgs() (*image.Context, error) { KubernetesArtefactDownloader: kubernetes.ArtefactDownloader{}, } + failedValidations := validation.ValidateDefinition(ctx) + if len(failedValidations) > 0 { + audit.Audit("Image definition validation found the following errors:") + + logMessageBuilder := strings.Builder{} + + orderedComponentNames := make([]string, 0, len(failedValidations)) + for c := range failedValidations { + orderedComponentNames = append(orderedComponentNames, c) + } + slices.Sort(orderedComponentNames) + + for _, componentName := range orderedComponentNames { + failures := failedValidations[componentName] + audit.Audit(fmt.Sprintf(" %s", componentName)) + for _, cf := range failures { + audit.Audit(fmt.Sprintf(" %s", cf.UserMessage)) + logMessageBuilder.WriteString(cf.UserMessage + "\n") + if cf.Error != nil { + logMessageBuilder.WriteString("\t" + cf.Error.Error() + "\n") + } + } + } + + if s := logMessageBuilder.String(); s != "" { + zap.S().Fatalf("Image definition validation failures:\n%s", s) + } + } + + if validate { + audit.Audit("The specified image definition is valid.") + os.Exit(0) + } + if !combustion.SkipRPMComponent(ctx) { p, err := podman.New(buildDir) if err != nil { @@ -109,11 +149,6 @@ func parseImageDefinition(configFile string, configDir string) (*image.Definitio return nil, fmt.Errorf("error parsing definition file \"%s\": %w", configFile, err) } - err = image.ValidateDefinition(imageDefinition) - if err != nil { - return nil, fmt.Errorf("error validating definition file: %w", err) - } - return imageDefinition, nil } diff --git a/pkg/image/validation/image.go b/pkg/image/validation/image.go index a0b9328e..fd78d25c 100644 --- a/pkg/image/validation/image.go +++ b/pkg/image/validation/image.go @@ -11,7 +11,7 @@ import ( ) const ( - imageComponent = "image" + imageComponent = "Image" ) func validateImage(ctx *image.Context) []FailedValidation { @@ -25,40 +25,34 @@ func validateImage(ctx *image.Context) []FailedValidation { if def.Image.ImageType == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'imageType' field is required in the 'image' section.", - Component: imageComponent, }) } else if !slices.Contains(validImageTypes, def.Image.ImageType) { msg := fmt.Sprintf("The 'imageType' field must be one of: %s", strings.Join(validImageTypes, ", ")) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: imageComponent, }) } if def.Image.Arch == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'arch' field is required in the 'image' section.", - Component: imageComponent, }) } else if !slices.Contains(validArchTypes, string(def.Image.Arch)) { msg := fmt.Sprintf("The 'arch' field must be one of: %s", strings.Join(validArchTypes, ", ")) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: imageComponent, }) } if def.Image.OutputImageName == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'outputImageName' field is required in the 'image' section.", - Component: imageComponent, }) } if def.Image.BaseImage == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'baseImage' field is required in the 'image' section.", - Component: imageComponent, }) } else { baseImageFilename := filepath.Join(ctx.ImageConfigDir, "images", def.Image.BaseImage) @@ -68,13 +62,11 @@ func validateImage(ctx *image.Context) []FailedValidation { msg := fmt.Sprintf("The specified base image '%s' cannot be found.", def.Image.BaseImage) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: imageComponent, }) } else { msg := fmt.Sprintf("The specified base image '%s' cannot be read. See the logs for more information.", def.Image.BaseImage) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: imageComponent, Error: err, }) } diff --git a/pkg/image/validation/image_test.go b/pkg/image/validation/image_test.go index 03a51db6..7b861055 100644 --- a/pkg/image/validation/image_test.go +++ b/pkg/image/validation/image_test.go @@ -92,7 +92,6 @@ func TestValidateImage(t *testing.T) { var foundMessages []string for _, foundValidation := range failedValidations { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, imageComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { diff --git a/pkg/image/validation/kubernetes.go b/pkg/image/validation/kubernetes.go index c5abff5b..527da0e5 100644 --- a/pkg/image/validation/kubernetes.go +++ b/pkg/image/validation/kubernetes.go @@ -45,14 +45,12 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation { if k8s.Network.APIVIP == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.", - Component: k8sComponent, }) } if k8s.Network.APIHost == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'apiHost' field is required in the 'network' section when defining entries under 'nodes'.", - Component: k8sComponent, }) } @@ -64,7 +62,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation { if node.Hostname == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'hostname' field is required for entries in the 'nodes' section.", - Component: k8sComponent, }) } @@ -73,7 +70,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation { msg := fmt.Sprintf("The 'type' field for entries in the 'nodes' section must be one of: %s", options) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: k8sComponent, }) } @@ -85,7 +81,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation { msg := fmt.Sprintf("The node labeled with 'initialiser' must be of type '%s'.", image.KubernetesNodeTypeServer) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: k8sComponent, }) } } @@ -99,7 +94,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation { msg := fmt.Sprintf("The 'nodes' section contains duplicate entries: %s", duplicateValues) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: k8sComponent, }) } @@ -107,14 +101,12 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation { msg := fmt.Sprintf("There must be at least one node of type '%s' defined.", image.KubernetesNodeTypeServer) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: k8sComponent, }) } if len(initialisers) > 1 { failures = append(failures, FailedValidation{ UserMessage: "Only one node may be specified as the cluster initializer.", - Component: k8sComponent, }) } @@ -133,7 +125,6 @@ func validateManifestURLs(k8s *image.Kubernetes) []FailedValidation { if !strings.HasPrefix(manifest, "http") { failures = append(failures, FailedValidation{ UserMessage: "Entries in 'urls' must begin with either 'http://' or 'https://'.", - Component: k8sComponent, }) } @@ -141,7 +132,6 @@ func validateManifestURLs(k8s *image.Kubernetes) []FailedValidation { msg := fmt.Sprintf("The 'urls' field contains duplicate entries: %s", manifest) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: k8sComponent, }) } diff --git a/pkg/image/validation/kubernetes_test.go b/pkg/image/validation/kubernetes_test.go index 88b6c491..2e06bcf5 100644 --- a/pkg/image/validation/kubernetes_test.go +++ b/pkg/image/validation/kubernetes_test.go @@ -78,7 +78,6 @@ func TestValidateKubernetes(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, k8sComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -295,7 +294,6 @@ func TestValidateNodes(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, k8sComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -369,7 +367,6 @@ func TestValidateManifestURLs(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, k8sComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { diff --git a/pkg/image/validation/os.go b/pkg/image/validation/os.go index 52195af0..a2c9a964 100644 --- a/pkg/image/validation/os.go +++ b/pkg/image/validation/os.go @@ -52,7 +52,6 @@ func validateKernelArgs(os *image.OperatingSystem) []FailedValidation { if key == "" || value == "" { failures = append(failures, FailedValidation{ UserMessage: "Kernel arguments must be specified as 'key=value'.", - Component: osComponent, }) } } @@ -60,7 +59,6 @@ func validateKernelArgs(os *image.OperatingSystem) []FailedValidation { if _, exists := seenKeys[key]; exists { failures = append(failures, FailedValidation{ UserMessage: fmt.Sprintf("Duplicate kernel argument found: %s", key), - Component: osComponent, }) } seenKeys[key] = true @@ -77,7 +75,6 @@ func validateSystemd(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("Systemd enable list contains duplicate entries: %s", duplicateValues) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } @@ -86,7 +83,6 @@ func validateSystemd(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("Systemd disable list contains duplicate entries: %s", duplicateValues) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } @@ -96,7 +92,6 @@ func validateSystemd(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("Systemd conflict found, '%s' is both enabled and disabled.", enableItem) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } } @@ -113,7 +108,6 @@ func validateUsers(os *image.OperatingSystem) []FailedValidation { if user.Username == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'username' field is required for all entries under 'users'.", - Component: osComponent, }) } @@ -121,7 +115,6 @@ func validateUsers(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("User '%s' must have either a password or SSH key.", user.Username) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } @@ -129,7 +122,6 @@ func validateUsers(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("Duplicate username found: %s", user.Username) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } seenUsernames[user.Username] = true @@ -147,19 +139,16 @@ func validateSuma(os *image.OperatingSystem) []FailedValidation { if os.Suma.Host == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'host' field is required for the 'suma' section.", - Component: osComponent, }) } if strings.HasPrefix(os.Suma.Host, "http") { failures = append(failures, FailedValidation{ UserMessage: "The suma 'host' field may not contain 'http://' or 'https://'", - Component: osComponent, }) } if os.Suma.ActivationKey == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'activationKey' field is required for the 'suma' section.", - Component: osComponent, }) } @@ -174,7 +163,6 @@ func validatePackages(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("The 'packageList' field contains duplicate packages: %s", duplicateValues) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } @@ -183,14 +171,12 @@ func validatePackages(os *image.OperatingSystem) []FailedValidation { msg := fmt.Sprintf("The 'additionalRepos' field contains duplicate repos: %s", duplicateValues) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } if len(os.Packages.PKGList) > 0 && len(os.Packages.AdditionalRepos) == 0 && os.Packages.RegCode == "" { failures = append(failures, FailedValidation{ UserMessage: "When including the 'packageList' field, either additional repositories or a registration code must be included.", - Component: osComponent, }) } @@ -204,7 +190,6 @@ func validateUnattended(def *image.Definition) []FailedValidation { msg := fmt.Sprintf("The 'unattended' field can only be used when 'imageType' is '%s'.", image.TypeISO) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } @@ -212,7 +197,6 @@ func validateUnattended(def *image.Definition) []FailedValidation { msg := fmt.Sprintf("The 'installDevice' field can only be used when 'imageType' is '%s'.", image.TypeISO) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: osComponent, }) } diff --git a/pkg/image/validation/os_test.go b/pkg/image/validation/os_test.go index b4c4c79a..02c3062c 100644 --- a/pkg/image/validation/os_test.go +++ b/pkg/image/validation/os_test.go @@ -94,6 +94,15 @@ func TestValidateOperatingSystem(t *testing.T) { } failures := validateOperatingSystem(&ctx) assert.Len(t, failures, len(test.ExpectedFailedMessages)) + + var foundMessages []string + for _, foundValidation := range failures { + foundMessages = append(foundMessages, foundValidation.UserMessage) + } + + for _, expectedMessage := range test.ExpectedFailedMessages { + assert.Contains(t, foundMessages, expectedMessage) + } }) } } @@ -193,7 +202,6 @@ func TestValidateKernelArgs(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, osComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -250,7 +258,6 @@ func TestValidateSystemd(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, osComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -323,7 +330,6 @@ func TestValidateUsers(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, osComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -385,7 +391,6 @@ func TestValidateSuma(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, osComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -448,7 +453,6 @@ func TestPackages(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, osComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -514,7 +518,6 @@ func TestValidateUnattended(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, osComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { diff --git a/pkg/image/validation/registry.go b/pkg/image/validation/registry.go index b77da194..9f92d934 100644 --- a/pkg/image/validation/registry.go +++ b/pkg/image/validation/registry.go @@ -28,7 +28,6 @@ func validateContainerImages(ear *image.EmbeddedArtifactRegistry) []FailedValida if cImage.Name == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'name' field is required for each entry in 'images'.", - Component: registryComponent, }) } @@ -36,7 +35,6 @@ func validateContainerImages(ear *image.EmbeddedArtifactRegistry) []FailedValida msg := fmt.Sprintf("Duplicate image name '%s' found in the 'images' section.", cImage.Name) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: registryComponent, }) } seenContainerImages[cImage.Name] = true @@ -54,26 +52,22 @@ func validateHelmCharts(ear *image.EmbeddedArtifactRegistry) []FailedValidation if chart.Name == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'name' field is required for each entry in 'charts'.", - Component: registryComponent, }) } if chart.RepoURL == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'repoURL' field is required for each entry in 'charts'.", - Component: registryComponent, }) } else if !strings.HasPrefix(chart.RepoURL, "http") { failures = append(failures, FailedValidation{ UserMessage: "The 'repoURL' field must begin with either 'http://' or 'https://'.", - Component: registryComponent, }) } if chart.Version == "" { failures = append(failures, FailedValidation{ UserMessage: "The 'version' field is required for each entry in 'charts'.", - Component: registryComponent, }) } @@ -81,7 +75,6 @@ func validateHelmCharts(ear *image.EmbeddedArtifactRegistry) []FailedValidation msg := fmt.Sprintf("Duplicate chart name '%s' found in the 'charts' section.", chart.Name) failures = append(failures, FailedValidation{ UserMessage: msg, - Component: registryComponent, }) } seenCharts[chart.Name] = true diff --git a/pkg/image/validation/registry_test.go b/pkg/image/validation/registry_test.go index d3b6ff4b..58ce7b3b 100644 --- a/pkg/image/validation/registry_test.go +++ b/pkg/image/validation/registry_test.go @@ -68,7 +68,6 @@ func TestValidateEmbeddedArtifactRegistry(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, registryComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -137,7 +136,6 @@ func TestValidateContainerImages(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, registryComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { @@ -227,7 +225,6 @@ func TestValidateHelmCharts(t *testing.T) { var foundMessages []string for _, foundValidation := range failures { foundMessages = append(foundMessages, foundValidation.UserMessage) - assert.Equal(t, registryComponent, foundValidation.Component) } for _, expectedMessage := range test.ExpectedFailedMessages { diff --git a/pkg/image/validation/validation.go b/pkg/image/validation/validation.go index c1663a10..b9800d73 100644 --- a/pkg/image/validation/validation.go +++ b/pkg/image/validation/validation.go @@ -5,25 +5,27 @@ import ( ) type FailedValidation struct { - Component string UserMessage string Error error } type validateComponent func(ctx *image.Context) []FailedValidation -func ValidateDefinition(ctx *image.Context) []FailedValidation { - var failures []FailedValidation +func ValidateDefinition(ctx *image.Context) map[string][]FailedValidation { + failures := map[string][]FailedValidation{} - validations := []validateComponent{ - validateImage, - validateOperatingSystem, - validateEmbeddedArtifactRegistry, - validateKubernetes, + validations := map[string]validateComponent{ + imageComponent: validateImage, + osComponent: validateOperatingSystem, + registryComponent: validateEmbeddedArtifactRegistry, + k8sComponent: validateKubernetes, } - for _, v := range validations { + for componentName, v := range validations { componentFailures := v(ctx) - failures = append(failures, componentFailures...) + + if len(componentFailures) > 0 { + failures[componentName] = componentFailures + } } return failures diff --git a/pkg/image/validation/validation_test.go b/pkg/image/validation/validation_test.go new file mode 100644 index 00000000..55f9c7aa --- /dev/null +++ b/pkg/image/validation/validation_test.go @@ -0,0 +1,117 @@ +package validation + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/suse-edge/edge-image-builder/pkg/image" +) + +func TestValidateDefinition(t *testing.T) { + configDir, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(configDir) + }() + + testImagesDir := filepath.Join(configDir, "images") + err = os.MkdirAll(testImagesDir, os.ModePerm) + require.NoError(t, err) + + fakeBaseImageName := "fake-base.iso" + _, err = os.Create(filepath.Join(testImagesDir, fakeBaseImageName)) + require.NoError(t, err) + + tests := map[string]struct { + Definition image.Definition + Expected map[string][]string + }{ + `minimal valid`: { + Definition: image.Definition{ + APIVersion: "1.0", + Image: image.Image{ + ImageType: "iso", + Arch: image.ArchTypeX86, + BaseImage: fakeBaseImageName, + OutputImageName: "output.iso", + }, + }, + }, + `one error from each`: { + Definition: image.Definition{ + APIVersion: "1.0", + Image: image.Image{ + Arch: image.ArchTypeX86, + BaseImage: fakeBaseImageName, + OutputImageName: "output.iso", + }, + OperatingSystem: image.OperatingSystem{ + KernelArgs: []string{"foo="}, + }, + EmbeddedArtifactRegistry: image.EmbeddedArtifactRegistry{ + ContainerImages: []image.ContainerImage{ + { + Name: "", // trips the missing name validation + }, + }, + }, + Kubernetes: image.Kubernetes{ + Network: image.Network{}, + Nodes: []image.Node{ + { + Hostname: "host1", + Type: image.KubernetesNodeTypeServer, + }, + { + Hostname: "host2", + Type: image.KubernetesNodeTypeAgent, + }, + }, + }, + }, + Expected: map[string][]string{ + imageComponent: { + "The 'imageType' field is required in the 'image' section.", + }, + osComponent: { + "Kernel arguments must be specified as 'key=value'.", + }, + registryComponent: { + "The 'name' field is required for each entry in 'images'.", + }, + k8sComponent: { + "The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.", + "The 'apiHost' field is required in the 'network' section when defining entries under 'nodes'.", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + def := test.Definition + ctx := image.Context{ + ImageDefinition: &def, + ImageConfigDir: configDir, + } + failures := ValidateDefinition(&ctx) + + for foundComponent, foundComponentFailures := range failures { + assert.Contains(t, test.Expected, foundComponent) + assert.Len(t, foundComponentFailures, len(test.Expected[foundComponent])) + + var foundMessages []string + for _, foundValidation := range foundComponentFailures { + foundMessages = append(foundMessages, foundValidation.UserMessage) + } + + for _, expectedMessage := range test.Expected[foundComponent] { + assert.Contains(t, foundMessages, expectedMessage) + } + } + }) + } +}