diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5247d47b..cbed41c1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -9,6 +9,7 @@ * Added the ability to automatically copy files into the built images filesystem (see Image Configuration Directory Changes below) * Kubernetes manifests are now applied in a systemd service instead of using the `/manifests` directory * Helm chart installation backOffLimit changed from 1000(default) to 20 +* Added Elemental configuration validation * Dropped `-chart` suffix from installed Helm chart names * Added caching for container images * Added built image name output to build command @@ -31,6 +32,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 ## Bug Fixes diff --git a/config/artifacts.yaml b/config/artifacts.yaml index 48a04c55..0d8b2296 100644 --- a/config/artifacts.yaml +++ b/config/artifacts.yaml @@ -6,9 +6,6 @@ endpoint-copier-operator: chart: endpoint-copier-operator repository: https://suse-edge.github.io/charts version: 0.2.1 -elemental: - register-repository: https://download.opensuse.org/repositories/isv:/Rancher:/Elemental:/Staging/standard - system-agent-repository: https://download.opensuse.org/repositories/isv:/Rancher:/Elemental:/Staging/standard kubernetes: k3s: selinuxPackage: k3s-selinux-1.6-1.slemicro.noarch diff --git a/docs/building-images.md b/docs/building-images.md index b9501f45..27534495 100644 --- a/docs/building-images.md +++ b/docs/building-images.md @@ -484,6 +484,9 @@ the built image and used to register with Elemental on boot. > To ensure a successful build, this process requires the ```--privileged``` flag to be passed to the > ```podman run``` command. For more info on why this is required, please see > [Package resolution design](design/pkg-resolution.md#running-the-eib-container). +> +> Additionally, when using SL Micro 6.0, an [`sccRegistrationCode`](#operating-system) must be provided in the `operatingSystem` section +> of the image definition so that the necessary Elemental RPMs can be downloaded. ## Operating System Files diff --git a/pkg/eib/eib.go b/pkg/eib/eib.go index f574f605..bcfdaecf 100644 --- a/pkg/eib/eib.go +++ b/pkg/eib/eib.go @@ -97,15 +97,8 @@ func appendElementalRPMs(ctx *image.Context) { } log.AuditInfo("Elemental registration is configured. The necessary RPM packages will be downloaded.") + appendRPMs(ctx, nil, combustion.ElementalPackages...) - appendRPMs(ctx, []image.AddRepo{ - { - URL: ctx.ArtifactSources.Elemental.RegisterRepository, - }, - { - URL: ctx.ArtifactSources.Elemental.SystemAgentRepository, - }, - }, combustion.ElementalPackages...) } func appendFips(ctx *image.Context) { diff --git a/pkg/image/context.go b/pkg/image/context.go index 6d8e0c0b..7f5202d1 100644 --- a/pkg/image/context.go +++ b/pkg/image/context.go @@ -35,10 +35,6 @@ type ArtifactSources struct { Repository string `yaml:"repository"` Version string `yaml:"version"` } `yaml:"endpoint-copier-operator"` - Elemental struct { - RegisterRepository string `yaml:"register-repository"` - SystemAgentRepository string `yaml:"system-agent-repository"` - } `yaml:"elemental"` Kubernetes struct { K3s struct { SELinuxPackage string `yaml:"selinuxPackage"` diff --git a/pkg/image/validation/elemental.go b/pkg/image/validation/elemental.go new file mode 100644 index 00000000..e7bbc36d --- /dev/null +++ b/pkg/image/validation/elemental.go @@ -0,0 +1,74 @@ +package validation + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/suse-edge/edge-image-builder/pkg/image" +) + +const ( + elementalComponent = "Elemental" + elementalConfigFilename = "elemental_config.yaml" +) + +func validateElemental(ctx *image.Context) []FailedValidation { + var failures []FailedValidation + + elementalConfigDir := filepath.Join(ctx.ImageConfigDir, "elemental") + if _, err := os.Stat(elementalConfigDir); err != nil { + if os.IsNotExist(err) { + return nil + } + + failures = append(failures, FailedValidation{ + UserMessage: "Elemental config directory could not be read", + Error: err, + }) + return failures + } + + 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 +} + +func validateElementalDir(elementalConfigDir string) []FailedValidation { + var failures []FailedValidation + + elementalConfigDirEntries, err := os.ReadDir(elementalConfigDir) + if err != nil { + failures = append(failures, FailedValidation{ + UserMessage: "Elemental config directory could not be read", + Error: err, + }) + + return failures + } + + switch len(elementalConfigDirEntries) { + case 0: + failures = append(failures, FailedValidation{ + UserMessage: "Elemental config directory should not be present if it is empty", + }) + case 1: + if elementalConfigDirEntries[0].Name() != elementalConfigFilename { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Elemental config file should only be named `%s`", elementalConfigFilename), + }) + } + default: + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Elemental config directory should only contain a singular '%s' file", elementalConfigFilename), + }) + } + + return failures +} diff --git a/pkg/image/validation/elemental_test.go b/pkg/image/validation/elemental_test.go new file mode 100644 index 00000000..7d032f09 --- /dev/null +++ b/pkg/image/validation/elemental_test.go @@ -0,0 +1,150 @@ +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 TestValidateElementalNoDir(t *testing.T) { + ctx := image.Context{} + + failures := validateElemental(&ctx) + assert.Len(t, failures, 0) +} + +func TestValidateElementalValid(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`: { + ImageDefinition: &image.Definition{ + OperatingSystem: image.OperatingSystem{ + Packages: image.Packages{ + RegCode: "registration-code", + }, + }, + }, + }, + `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", + }, + }, + } + + 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 TestValidateElementalConfigDirValid(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)) + + elementalConfig := filepath.Join(elementalDir, "elemental_config.yaml") + require.NoError(t, os.WriteFile(elementalConfig, []byte(""), 0o600)) + + failures := validateElementalDir(elementalDir) + assert.Len(t, failures, 0) +} + +func TestValidateElementalConfigDirEmptyDir(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)) + + failures := validateElementalDir(elementalDir) + assert.Len(t, failures, 1) + + assert.Contains(t, failures[0].UserMessage, "Elemental config directory should not be present if it is empty") +} + +func TestValidateElementalConfigDirMultipleFiles(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)) + + firstElementalConfig := filepath.Join(elementalDir, "elemental_config1.yaml") + require.NoError(t, os.WriteFile(firstElementalConfig, []byte(""), 0o600)) + secondElementalConfig := filepath.Join(elementalDir, "elemental_config2.yaml") + require.NoError(t, os.WriteFile(secondElementalConfig, []byte(""), 0o600)) + + failures := validateElementalDir(elementalDir) + assert.Len(t, failures, 1) + + assert.Contains(t, failures[0].UserMessage, "Elemental config directory should only contain a singular 'elemental_config.yaml' file") +} + +func TestValidateElementalConfigDirUnreadable(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)) + require.NoError(t, os.Chmod(elementalDir, 0o333)) + + failures := validateElementalDir(elementalDir) + assert.Len(t, failures, 1) + + assert.Contains(t, failures[0].UserMessage, "Elemental config directory could not be read") +} diff --git a/pkg/image/validation/validation.go b/pkg/image/validation/validation.go index eddf12b2..7f08ccc4 100644 --- a/pkg/image/validation/validation.go +++ b/pkg/image/validation/validation.go @@ -15,11 +15,12 @@ func ValidateDefinition(ctx *image.Context) map[string][]FailedValidation { failures := map[string][]FailedValidation{} validations := map[string]validateComponent{ - versionComponent: validateVersion, - imageComponent: validateImage, - osComponent: validateOperatingSystem, - registryComponent: validateEmbeddedArtifactRegistry, - k8sComponent: validateKubernetes, + versionComponent: validateVersion, + imageComponent: validateImage, + osComponent: validateOperatingSystem, + registryComponent: validateEmbeddedArtifactRegistry, + k8sComponent: validateKubernetes, + elementalComponent: validateElemental, } for componentName, v := range validations { componentFailures := v(ctx)