diff --git a/pkg/combustion/registry.go b/pkg/combustion/registry.go index 18ccb96e..f232d088 100644 --- a/pkg/combustion/registry.go +++ b/pkg/combustion/registry.go @@ -30,7 +30,7 @@ var haulerManifest string var registryScript string func configureRegistry(ctx *image.Context) ([]string, error) { - if image.IsEmbeddedArtifactRegistryEmpty(ctx.ImageDefinition.EmbeddedArtifactRegistry) { + if IsEmbeddedArtifactRegistryEmpty(ctx.ImageDefinition.EmbeddedArtifactRegistry) { log.AuditComponentSkipped(registryComponentName) return nil, nil } @@ -172,3 +172,7 @@ func createRegistryCommand(ctx *image.Context, commandName string, args []string return cmd, logFile, nil } + +func IsEmbeddedArtifactRegistryEmpty(registry image.EmbeddedArtifactRegistry) bool { + return len(registry.HelmCharts) == 0 && len(registry.ContainerImages) == 0 +} diff --git a/pkg/combustion/registry_test.go b/pkg/combustion/registry_test.go index b88a272d..b65e8878 100644 --- a/pkg/combustion/registry_test.go +++ b/pkg/combustion/registry_test.go @@ -137,3 +137,67 @@ func TestCopyHaulerBinaryNoFile(t *testing.T) { // Verify require.ErrorContains(t, err, "no such file") } + +func TestIsEmbeddedArtifactRegistryEmpty(t *testing.T) { + tests := []struct { + name string + registry image.EmbeddedArtifactRegistry + isEmpty bool + }{ + { + name: "Both Defined", + registry: image.EmbeddedArtifactRegistry{ + HelmCharts: []image.HelmChart{ + { + Name: "rancher", + RepoURL: "https://releases.rancher.com/server-charts/stable", + Version: "2.8.0", + }, + }, + ContainerImages: []image.ContainerImage{ + { + Name: "hello-world:latest", + SupplyChainKey: "", + }, + }, + }, + isEmpty: false, + }, + { + name: "Chart Defined", + registry: image.EmbeddedArtifactRegistry{ + HelmCharts: []image.HelmChart{ + { + Name: "rancher", + RepoURL: "https://releases.rancher.com/server-charts/stable", + Version: "2.8.0", + }, + }, + }, + isEmpty: false, + }, + { + name: "Image Defined", + registry: image.EmbeddedArtifactRegistry{ + ContainerImages: []image.ContainerImage{ + { + Name: "hello-world:latest", + SupplyChainKey: "", + }, + }, + }, + isEmpty: false, + }, + { + name: "None Defined", + isEmpty: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := IsEmbeddedArtifactRegistryEmpty(test.registry) + assert.Equal(t, test.isEmpty, result) + }) + } +} diff --git a/pkg/image/validation/registry.go b/pkg/image/validation/registry.go new file mode 100644 index 00000000..74043a25 --- /dev/null +++ b/pkg/image/validation/registry.go @@ -0,0 +1,97 @@ +package validation + +import ( + "fmt" + "strings" + + "github.com/suse-edge/edge-image-builder/pkg/combustion" + "github.com/suse-edge/edge-image-builder/pkg/image" +) + +const ( + registryComponent = "Artifact Registry" +) + +func validateEmbeddedArtefactRegistry(ctx *image.Context) []FailedValidation { + var failures []FailedValidation + def := ctx.ImageDefinition + + if combustion.IsEmbeddedArtifactRegistryEmpty(def.EmbeddedArtifactRegistry) { + return failures + } + + failures = append(failures, validateContainerImages(&ctx.ImageDefinition.EmbeddedArtifactRegistry)...) + failures = append(failures, validateHelmCharts(&ctx.ImageDefinition.EmbeddedArtifactRegistry)...) + + return failures +} + +func validateContainerImages(ear *image.EmbeddedArtifactRegistry) []FailedValidation { + var failures []FailedValidation + + seenContainerImages := make(map[string]bool) + for _, cImage := range ear.ContainerImages { + if cImage.Name == "" { + failures = append(failures, FailedValidation{ + userMessage: "The 'name' field is required for each entry in 'images'.", + component: registryComponent, + }) + } + + if seenContainerImages[cImage.Name] { + 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 + } + + return failures +} + +func validateHelmCharts(ear *image.EmbeddedArtifactRegistry) []FailedValidation { + var failures []FailedValidation + + charts := ear.HelmCharts + seenCharts := make(map[string]bool) + for _, chart := range charts { + 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, + }) + } + + if seenCharts[chart.Name] { + 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 + } + + return failures +} diff --git a/pkg/image/validation/registry_test.go b/pkg/image/validation/registry_test.go new file mode 100644 index 00000000..4f2e98df --- /dev/null +++ b/pkg/image/validation/registry_test.go @@ -0,0 +1,238 @@ +package validation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/suse-edge/edge-image-builder/pkg/image" +) + +func TestValidateEmbeddedArtefactRegistry(t *testing.T) { + tests := map[string]struct { + Registry image.EmbeddedArtifactRegistry + ExpectedFailedMessages []string + }{ + `no registry`: { + Registry: image.EmbeddedArtifactRegistry{}, + }, + `full valid example`: { + Registry: image.EmbeddedArtifactRegistry{ + ContainerImages: []image.ContainerImage{ + { + Name: "foo", + SupplyChainKey: "key", + }, + }, + HelmCharts: []image.HelmChart{ + { + Name: "bar", + RepoURL: "http://bar.com", + Version: "3.14", + }, + }, + }, + }, + `failures in both sections`: { + Registry: image.EmbeddedArtifactRegistry{ + ContainerImages: []image.ContainerImage{ + { + Name: "", // trips the missing name validation + }, + }, + HelmCharts: []image.HelmChart{ + { + Name: "", // trips the missing name validation + RepoURL: "http://doesntmatter.com", + Version: "31", + }, + }, + }, + ExpectedFailedMessages: []string{ + "The 'name' field is required for each entry in 'images'.", + "The 'name' field is required for each entry in 'charts'.", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ear := test.Registry + ctx := image.Context{ + ImageDefinition: &image.Definition{ + EmbeddedArtifactRegistry: ear, + }, + } + failures := validateEmbeddedArtefactRegistry(&ctx) + assert.Len(t, failures, len(test.ExpectedFailedMessages)) + + var foundMessages []string + for _, foundValidation := range failures { + foundMessages = append(foundMessages, foundValidation.userMessage) + assert.Equal(t, registryComponent, foundValidation.component) + } + + for _, expectedMessage := range test.ExpectedFailedMessages { + assert.Contains(t, foundMessages, expectedMessage) + } + }) + } +} + +func TestValidateContainerImages(t *testing.T) { + tests := map[string]struct { + Registry image.EmbeddedArtifactRegistry + ExpectedFailedMessages []string + }{ + `no images`: { + Registry: image.EmbeddedArtifactRegistry{}, + }, + `missing name`: { + Registry: image.EmbeddedArtifactRegistry{ + ContainerImages: []image.ContainerImage{ + { + Name: "valid", + }, + { + Name: "", + }, + }, + }, + ExpectedFailedMessages: []string{ + "The 'name' field is required for each entry in 'images'.", + }, + }, + `duplicate name`: { + Registry: image.EmbeddedArtifactRegistry{ + ContainerImages: []image.ContainerImage{ + { + Name: "foo", + }, + { + Name: "bar", + }, + { + Name: "foo", + }, + { + Name: "baz", + }, + { + Name: "bar", + }, + }, + }, + ExpectedFailedMessages: []string{ + "Duplicate image name 'foo' found in the 'images' section.", + "Duplicate image name 'bar' found in the 'images' section.", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ear := test.Registry + failures := validateContainerImages(&ear) + assert.Len(t, failures, len(test.ExpectedFailedMessages)) + + var foundMessages []string + for _, foundValidation := range failures { + foundMessages = append(foundMessages, foundValidation.userMessage) + assert.Equal(t, registryComponent, foundValidation.component) + } + + for _, expectedMessage := range test.ExpectedFailedMessages { + assert.Contains(t, foundMessages, expectedMessage) + } + }) + } +} + +func TestValidateHelmCharts(t *testing.T) { + tests := map[string]struct { + Registry image.EmbeddedArtifactRegistry + ExpectedFailedMessages []string + }{ + `no helm charts`: { + Registry: image.EmbeddedArtifactRegistry{}, + }, + `valid charts`: { + Registry: image.EmbeddedArtifactRegistry{ + HelmCharts: []image.HelmChart{ + { + Name: "foo", + RepoURL: "http://valid.com", // shows http:// is allowed + Version: "1.0", + }, + { + Name: "bar", + RepoURL: "https://valid.com", // shows https:// is allowed + Version: "2.0", + }, + }, + }, + }, + `missing fields`: { + Registry: image.EmbeddedArtifactRegistry{ + HelmCharts: []image.HelmChart{ + {}, + }, + }, + ExpectedFailedMessages: []string{ + "The 'name' field is required for each entry in 'charts'.", + "The 'repoURL' field is required for each entry in 'charts'.", + "The 'version' field is required for each entry in 'charts'.", + }, + }, + `duplicate chart`: { + Registry: image.EmbeddedArtifactRegistry{ + HelmCharts: []image.HelmChart{ + { + Name: "foo", + RepoURL: "http://foo.com", + Version: "1.0", + }, + { + Name: "foo", + RepoURL: "https://bar.com", + Version: "2.0", + }, + }, + }, + ExpectedFailedMessages: []string{ + "Duplicate chart name 'foo' found in the 'charts' section.", + }, + }, + `invalid repo`: { + Registry: image.EmbeddedArtifactRegistry{ + HelmCharts: []image.HelmChart{ + { + Name: "foo", + RepoURL: "example.com", + Version: "1.0", + }, + }, + }, + ExpectedFailedMessages: []string{ + "The 'repoURL' field must begin with either 'http://' or 'https://'.", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ear := test.Registry + failures := validateHelmCharts(&ear) + assert.Len(t, failures, len(test.ExpectedFailedMessages)) + + var foundMessages []string + for _, foundValidation := range failures { + foundMessages = append(foundMessages, foundValidation.userMessage) + assert.Equal(t, registryComponent, foundValidation.component) + } + + for _, expectedMessage := range test.ExpectedFailedMessages { + assert.Contains(t, foundMessages, expectedMessage) + } + }) + } +} diff --git a/pkg/image/validation/validation.go b/pkg/image/validation/validation.go index 8eabcb01..62ab3b0b 100644 --- a/pkg/image/validation/validation.go +++ b/pkg/image/validation/validation.go @@ -18,6 +18,7 @@ func ValidateDefinition(ctx *image.Context) []FailedValidation { validations := []validateComponent{ validateImage, validateOperatingSystem, + validateEmbeddedArtefactRegistry, } for _, v := range validations { componentFailures := v(ctx) diff --git a/pkg/image/validation_test.go b/pkg/image/validation_test.go index bf6e2540..606db7a4 100644 --- a/pkg/image/validation_test.go +++ b/pkg/image/validation_test.go @@ -848,70 +848,6 @@ func TestValidateCharts(t *testing.T) { } } -func TestIsEmbeddedArtifactRegistryEmpty(t *testing.T) { - tests := []struct { - name string - registry EmbeddedArtifactRegistry - isEmpty bool - }{ - { - name: "Both Defined", - registry: EmbeddedArtifactRegistry{ - HelmCharts: []HelmChart{ - { - Name: "rancher", - RepoURL: "https://releases.rancher.com/server-charts/stable", - Version: "2.8.0", - }, - }, - ContainerImages: []ContainerImage{ - { - Name: "hello-world:latest", - SupplyChainKey: "", - }, - }, - }, - isEmpty: false, - }, - { - name: "Chart Defined", - registry: EmbeddedArtifactRegistry{ - HelmCharts: []HelmChart{ - { - Name: "rancher", - RepoURL: "https://releases.rancher.com/server-charts/stable", - Version: "2.8.0", - }, - }, - }, - isEmpty: false, - }, - { - name: "Image Defined", - registry: EmbeddedArtifactRegistry{ - ContainerImages: []ContainerImage{ - { - Name: "hello-world:latest", - SupplyChainKey: "", - }, - }, - }, - isEmpty: false, - }, - { - name: "None Defined", - isEmpty: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - result := IsEmbeddedArtifactRegistryEmpty(test.registry) - assert.Equal(t, test.isEmpty, result) - }) - } -} - func TestValidatePackages(t *testing.T) { tests := []struct { name string