diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 40705a36..c208e5da 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,6 +10,8 @@ * Helm chart installation backOffLimit changed from 1000(default) to 20 * Improved Kubernetes resource installation handling * Ensure that kernel arguments are applied during firstboot when kexec is used in ISO installations +* Improved Elemental handling when using SL Micro 6.0 +* Added Elemental configuration validation ## API diff --git a/pkg/eib/eib.go b/pkg/eib/eib.go index e6ef7838..4cd71da1 100644 --- a/pkg/eib/eib.go +++ b/pkg/eib/eib.go @@ -97,14 +97,19 @@ func appendElementalRPMs(ctx *image.Context) { log.AuditInfo("Elemental registration is configured. The necessary RPM packages will be downloaded.") - appendRPMs(ctx, []image.AddRepo{ - { - URL: ctx.ArtifactSources.Elemental.RegisterRepository, - }, - { - URL: ctx.ArtifactSources.Elemental.SystemAgentRepository, - }, - }, combustion.ElementalPackages...) + if ctx.ImageDefinition.APIVersion == "1.0" { + appendRPMs(ctx, []image.AddRepo{ + { + URL: ctx.ArtifactSources.Elemental.RegisterRepository, + }, + { + URL: ctx.ArtifactSources.Elemental.SystemAgentRepository, + }, + }, combustion.ElementalPackages...) + } else if ctx.ImageDefinition.APIVersion == "1.1" { + appendRPMs(ctx, nil, combustion.ElementalPackages...) + } + } func appendRPMs(ctx *image.Context, repos []image.AddRepo, packages ...string) { diff --git a/pkg/image/validation/elemental.go b/pkg/image/validation/elemental.go new file mode 100644 index 00000000..68165d0c --- /dev/null +++ b/pkg/image/validation/elemental.go @@ -0,0 +1,67 @@ +package validation + +import ( + "errors" + "fmt" + "github.com/suse-edge/edge-image-builder/pkg/image" + "go.uber.org/zap" + "os" + "path/filepath" +) + +const ( + elementalComponent = "Elemental" +) + +func validateElemental(ctx *image.Context) []FailedValidation { + var failures []FailedValidation + + elementalConfigDir := filepath.Join(ctx.ImageConfigDir, "elemental") + failures = append(failures, validateElementalDir(elementalConfigDir)...) + + if ctx.ImageDefinition.APIVersion == "1.1" && ctx.ImageDefinition.OperatingSystem.Packages.RegCode == "" { + failures = append(failures, FailedValidation{ + UserMessage: "Operating system package registration code field must be defined when using Elemental with SL Micro 6.0", + }) + } + + return failures +} + +func validateElementalDir(elementalConfigDir string) []FailedValidation { + var failures []FailedValidation + + elementalConfigDirEntries, err := os.ReadDir(elementalConfigDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + + zap.S().Errorf("Elemental config directory could not be read: %s", err) + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Elemental config directory could not be read: %s", err), + }) + } + + if len(elementalConfigDirEntries) == 0 { + failures = append(failures, FailedValidation{ + UserMessage: "Elemental config directory should not be present if it is empty", + }) + } + + if len(elementalConfigDirEntries) > 1 { + failures = append(failures, FailedValidation{ + UserMessage: "Elemental config directory should only contain a singular 'elemental_config.yaml' file", + }) + } + + if len(elementalConfigDirEntries) == 1 { + if elementalConfigDirEntries[0].Name() != "elemental_config.yaml" { + failures = append(failures, FailedValidation{ + UserMessage: "Elemental config file should only be named `elemental_config.yaml`", + }) + } + } + + return failures +} diff --git a/pkg/image/validation/elemental_test.go b/pkg/image/validation/elemental_test.go new file mode 100644 index 00000000..52609930 --- /dev/null +++ b/pkg/image/validation/elemental_test.go @@ -0,0 +1,174 @@ +package validation + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/suse-edge/edge-image-builder/pkg/image" + "os" + "path/filepath" + "testing" +) + +func TestValidateElemental(t *testing.T) { + configDir, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, os.RemoveAll(configDir)) + }() + + elementalDir := filepath.Join(configDir, "elemental") + require.NoError(t, os.MkdirAll(elementalDir, os.ModePerm)) + + validElementalConfig := filepath.Join(elementalDir, "elemental_config.yaml") + require.NoError(t, os.WriteFile(validElementalConfig, []byte(""), 0o600)) + + tests := map[string]struct { + ImageDefinition image.Definition + ExpectedFailedMessages []string + }{ + `valid 1.1`: { + ImageDefinition: image.Definition{ + APIVersion: "1.1", + OperatingSystem: image.OperatingSystem{ + Packages: image.Packages{ + RegCode: "registration-code", + }, + }, + }, + }, + `1.1 no registration code`: { + ImageDefinition: image.Definition{ + APIVersion: "1.1", + }, + ExpectedFailedMessages: []string{ + "Operating system package registration code field must be defined when using Elemental with SL Micro 6.0", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := image.Context{ + ImageConfigDir: configDir, + ImageDefinition: &test.ImageDefinition, + } + failures := validateElemental(&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) + } + + }) + } +} + +func TestValidateElementalConfigDir(t *testing.T) { + configDirValid, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + configDirEmpty, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + configDirMultipleFiles, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + configDirInvalidName, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + configDirUnreadable, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, os.RemoveAll(configDirValid)) + assert.NoError(t, os.RemoveAll(configDirEmpty)) + assert.NoError(t, os.RemoveAll(configDirMultipleFiles)) + assert.NoError(t, os.RemoveAll(configDirInvalidName)) + assert.NoError(t, os.RemoveAll(configDirUnreadable)) + }() + + elementalDirValid := filepath.Join(configDirValid, "elemental") + require.NoError(t, os.MkdirAll(elementalDirValid, os.ModePerm)) + + validElementalConfig := filepath.Join(elementalDirValid, "elemental_config.yaml") + require.NoError(t, os.WriteFile(validElementalConfig, []byte(""), 0o600)) + + elementalDirEmpty := filepath.Join(configDirEmpty, "elemental") + require.NoError(t, os.MkdirAll(elementalDirEmpty, os.ModePerm)) + + elementalDirMultipleFiles := filepath.Join(configDirMultipleFiles, "elemental") + require.NoError(t, os.MkdirAll(elementalDirMultipleFiles, os.ModePerm)) + + firstElementalConfig := filepath.Join(elementalDirMultipleFiles, "elemental_config1.yaml") + require.NoError(t, os.WriteFile(firstElementalConfig, []byte(""), 0o600)) + secondElementalConfig := filepath.Join(elementalDirMultipleFiles, "elemental_config2.yaml") + require.NoError(t, os.WriteFile(secondElementalConfig, []byte(""), 0o600)) + + elementalDirInvalidName := filepath.Join(configDirInvalidName, "elemental") + require.NoError(t, os.MkdirAll(elementalDirInvalidName, os.ModePerm)) + + invalidElementalConfig := filepath.Join(elementalDirInvalidName, "elemental.yaml") + require.NoError(t, os.WriteFile(invalidElementalConfig, []byte(""), 0o600)) + + elementalDirUnreadable := filepath.Join(configDirUnreadable, "elemental") + require.NoError(t, os.MkdirAll(elementalDirUnreadable, os.ModePerm)) + require.NoError(t, os.Chmod(elementalDirUnreadable, 0333)) + + tests := map[string]struct { + ExpectedFailedMessages []string + ElementalDir string + }{ + `valid elemental dir`: { + ElementalDir: elementalDirValid, + }, + `empty elemental dir`: { + ElementalDir: elementalDirEmpty, + ExpectedFailedMessages: []string{ + "Elemental config directory should not be present if it is empty", + }, + }, + `multiple files in elemental dir`: { + ElementalDir: elementalDirMultipleFiles, + ExpectedFailedMessages: []string{ + "Elemental config directory should only contain a singular 'elemental_config.yaml' file", + }, + }, + `invalid name in elemental dir`: { + ElementalDir: elementalDirInvalidName, + ExpectedFailedMessages: []string{ + "Elemental config file should only be named `elemental_config.yaml`", + }, + }, + `unreadable elemental dir`: { + ElementalDir: elementalDirUnreadable, + ExpectedFailedMessages: []string{ + fmt.Sprintf("Elemental config directory could not be read: open %s: permission denied", elementalDirUnreadable), + "Elemental config directory should not be present if it is empty", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + failures := validateElementalDir(test.ElementalDir) + 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) + } + + }) + } +} diff --git a/pkg/image/validation/validation.go b/pkg/image/validation/validation.go index eddf12b2..a149ef7c 100644 --- a/pkg/image/validation/validation.go +++ b/pkg/image/validation/validation.go @@ -16,10 +16,11 @@ func ValidateDefinition(ctx *image.Context) map[string][]FailedValidation { validations := map[string]validateComponent{ versionComponent: validateVersion, - imageComponent: validateImage, - osComponent: validateOperatingSystem, - registryComponent: validateEmbeddedArtifactRegistry, - k8sComponent: validateKubernetes, + imageComponent: validateImage, + osComponent: validateOperatingSystem, + registryComponent: validateEmbeddedArtifactRegistry, + k8sComponent: validateKubernetes, + elementalComponent: validateElemental, } for componentName, v := range validations { componentFailures := v(ctx)