diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cbed41c1..2f349d3b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -33,6 +33,7 @@ * An optional directory named `os-files` may be included to copy files into the resulting image's filesystem at runtime * The `custom/files` directory may now include subdirectories, which will be maintained when copied to the image * Elemental configuration now requires a registration code in order to install the necessary RPMs from the official sources + * Alternatively, the necessary Elemental RPMs can be manually side-loaded instead ## Bug Fixes diff --git a/pkg/eib/eib.go b/pkg/eib/eib.go index bcfdaecf..3c06c231 100644 --- a/pkg/eib/eib.go +++ b/pkg/eib/eib.go @@ -6,6 +6,8 @@ import ( "io/fs" "os" "path/filepath" + "slices" + "strings" "time" "github.com/suse-edge/edge-image-builder/pkg/build" @@ -96,9 +98,18 @@ func appendElementalRPMs(ctx *image.Context) { return } - log.AuditInfo("Elemental registration is configured. The necessary RPM packages will be downloaded.") - appendRPMs(ctx, nil, combustion.ElementalPackages...) + rpmsPath := combustion.RPMsPath(ctx) + rpmDirEntries, err := os.ReadDir(rpmsPath) + if err != nil && !os.IsNotExist(err) { + zap.S().Warnf("Looking for '%s' dir failed unexpectedly: %s", rpmsPath, err) + } + if !slices.ContainsFunc(rpmDirEntries, func(entry os.DirEntry) bool { + return strings.Contains(entry.Name(), combustion.ElementalPackages[0]) + }) { + log.AuditInfo("Elemental registration is configured. The necessary RPM packages will be downloaded.") + appendRPMs(ctx, nil, combustion.ElementalPackages...) + } } func appendFips(ctx *image.Context) { diff --git a/pkg/image/validation/elemental.go b/pkg/image/validation/elemental.go index e7bbc36d..5dba8ac4 100644 --- a/pkg/image/validation/elemental.go +++ b/pkg/image/validation/elemental.go @@ -4,7 +4,10 @@ import ( "fmt" "os" "path/filepath" + "slices" + "strings" + "github.com/suse-edge/edge-image-builder/pkg/combustion" "github.com/suse-edge/edge-image-builder/pkg/image" ) @@ -29,14 +32,9 @@ func validateElemental(ctx *image.Context) []FailedValidation { return failures } + failures = append(failures, validateElementalConfiguration(ctx)...) failures = append(failures, validateElementalDir(elementalConfigDir)...) - if 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 } @@ -72,3 +70,42 @@ func validateElementalDir(elementalConfigDir string) []FailedValidation { return failures } + +func validateElementalConfiguration(ctx *image.Context) []FailedValidation { + var failures []FailedValidation + + rpmDirEntries, err := os.ReadDir(combustion.RPMsPath(ctx)) + if err != nil && !os.IsNotExist(err) { + failures = append(failures, FailedValidation{ + UserMessage: "RPM directory could not be read", + Error: err, + }) + } + + var foundPackages []string + var notFoundPackages []string + for _, pkg := range combustion.ElementalPackages { + if slices.ContainsFunc(rpmDirEntries, func(entry os.DirEntry) bool { + return strings.Contains(entry.Name(), pkg) + }) { + foundPackages = append(foundPackages, pkg) + } else { + notFoundPackages = append(notFoundPackages, pkg) + } + } + + if len(foundPackages) == 0 { + if ctx.ImageDefinition.OperatingSystem.Packages.RegCode == "" { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Operating system package registration code field must be defined when using Elemental "+ + "or the %s RPMs must be manually side-loaded", combustion.ElementalPackages), + }) + } + } else if len(foundPackages) != len(combustion.ElementalPackages) { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Not all of the necessary Elemental packages are provided, packages found: %s, packages missing: %s", foundPackages, notFoundPackages), + }) + } + + return failures +} diff --git a/pkg/image/validation/elemental_test.go b/pkg/image/validation/elemental_test.go index 7d032f09..4b9cdaf1 100644 --- a/pkg/image/validation/elemental_test.go +++ b/pkg/image/validation/elemental_test.go @@ -47,7 +47,8 @@ func TestValidateElementalValid(t *testing.T) { `no registration code`: { ImageDefinition: &image.Definition{}, ExpectedFailedMessages: []string{ - "Operating system package registration code field must be defined when using Elemental with SL Micro 6.0", + "Operating system package registration code field must be defined when using Elemental or the " + + "[elemental-register elemental-system-agent] RPMs must be manually side-loaded", }, }, } @@ -148,3 +149,135 @@ func TestValidateElementalConfigDirUnreadable(t *testing.T) { assert.Contains(t, failures[0].UserMessage, "Elemental config directory could not be read") } + +func TestValidateElementalConfigurationManualRPMsNoRegistrationCode(t *testing.T) { + configDir, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, os.RemoveAll(configDir)) + }() + + ctx := &image.Context{ + ImageConfigDir: configDir, + ImageDefinition: &image.Definition{}, + } + + elementalDir := filepath.Join(configDir, "elemental") + require.NoError(t, os.MkdirAll(elementalDir, os.ModePerm)) + + elementalConfig := filepath.Join(elementalDir, "elemental_config.yaml") + require.NoError(t, os.WriteFile(elementalConfig, []byte(""), 0o600)) + + rpmDir := filepath.Join(configDir, "rpms") + require.NoError(t, os.MkdirAll(rpmDir, os.ModePerm)) + + elementalAgent := filepath.Join(rpmDir, "elemental-system-agent.rpm") + require.NoError(t, os.WriteFile(elementalAgent, []byte(""), 0o600)) + + elementalRegister := filepath.Join(rpmDir, "elemental-register.rpm") + require.NoError(t, os.WriteFile(elementalRegister, []byte(""), 0o600)) + + failures := validateElementalConfiguration(ctx) + assert.Len(t, failures, 0) +} + +func TestValidateElementalConfigurationManualRPMsWithRegistrationCode(t *testing.T) { + configDir, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, os.RemoveAll(configDir)) + }() + + ctx := &image.Context{ + ImageConfigDir: configDir, + ImageDefinition: &image.Definition{ + OperatingSystem: image.OperatingSystem{ + Packages: image.Packages{ + RegCode: "registration-code", + }, + }, + }, + } + + elementalDir := filepath.Join(configDir, "elemental") + require.NoError(t, os.MkdirAll(elementalDir, os.ModePerm)) + + elementalConfig := filepath.Join(elementalDir, "elemental_config.yaml") + require.NoError(t, os.WriteFile(elementalConfig, []byte(""), 0o600)) + + rpmDir := filepath.Join(configDir, "rpms") + require.NoError(t, os.MkdirAll(rpmDir, os.ModePerm)) + + elementalAgent := filepath.Join(rpmDir, "elemental-system-agent.rpm") + require.NoError(t, os.WriteFile(elementalAgent, []byte(""), 0o600)) + + elementalRegister := filepath.Join(rpmDir, "elemental-register.rpm") + require.NoError(t, os.WriteFile(elementalRegister, []byte(""), 0o600)) + + failures := validateElementalConfiguration(ctx) + assert.Len(t, failures, 0) +} + +func TestValidateElementalConfigurationManualRPMsMissingAgent(t *testing.T) { + configDir, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, os.RemoveAll(configDir)) + }() + + ctx := &image.Context{ + ImageConfigDir: configDir, + ImageDefinition: &image.Definition{}, + } + + elementalDir := filepath.Join(configDir, "elemental") + require.NoError(t, os.MkdirAll(elementalDir, os.ModePerm)) + + elementalConfig := filepath.Join(elementalDir, "elemental_config.yaml") + require.NoError(t, os.WriteFile(elementalConfig, []byte(""), 0o600)) + + rpmDir := filepath.Join(configDir, "rpms") + require.NoError(t, os.MkdirAll(rpmDir, os.ModePerm)) + + elementalRegister := filepath.Join(rpmDir, "elemental-register.rpm") + require.NoError(t, os.WriteFile(elementalRegister, []byte(""), 0o600)) + + failures := validateElementalConfiguration(ctx) + assert.Len(t, failures, 1) + + assert.Contains(t, failures[0].UserMessage, "Not all of the necessary Elemental packages are provided, packages found: [elemental-register], packages missing: [elemental-system-agent]") +} + +func TestValidateElementalConfigurationManualRPMsMissingRegister(t *testing.T) { + configDir, err := os.MkdirTemp("", "eib-config-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, os.RemoveAll(configDir)) + }() + + ctx := &image.Context{ + ImageConfigDir: configDir, + ImageDefinition: &image.Definition{}, + } + + elementalDir := filepath.Join(configDir, "elemental") + require.NoError(t, os.MkdirAll(elementalDir, os.ModePerm)) + + elementalConfig := filepath.Join(elementalDir, "elemental_config.yaml") + require.NoError(t, os.WriteFile(elementalConfig, []byte(""), 0o600)) + + rpmDir := filepath.Join(configDir, "rpms") + require.NoError(t, os.MkdirAll(rpmDir, os.ModePerm)) + + elementalAgent := filepath.Join(rpmDir, "elemental-system-agent.rpm") + require.NoError(t, os.WriteFile(elementalAgent, []byte(""), 0o600)) + + failures := validateElementalConfiguration(ctx) + assert.Len(t, failures, 1) + + assert.Contains(t, failures[0].UserMessage, "Not all of the necessary Elemental packages are provided, packages found: [elemental-system-agent], packages missing: [elemental-register]") +}