diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 968c96f1..cbed41c1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,34 +1,20 @@ # Edge Image Builder Releases -# Next +# v1.1.0 ## General -* Extracted the K3S and RKE2 SELinux package and repository definitions into artifacts.yaml - -## API - -### Image Definition Changes - -### Image Configuration Directory Changes - -## Bug Fixes - -* [#565](https://github.com/suse-edge/edge-image-builder/issues/565) - K3S SELinux uses an outdated package - ---- - -# v1.1.0-rc2 - -## General - -* The "custom files" functionality may now include directories, which will be maintained when copied to the image -* Improved Kubernetes definition validation -* Allow RKE2 deployments with Calico, Cilium and Multus on aarch64 platforms -* OS files and user provided certificates now maintain original permissions when copied to the final image +* Adds support for customizing SL Micro 6.0 base images (for SLE Micro 5.5 images, EIB 1.0.x must still be used) +* Added the ability to build aarch64 images on an aarch64 host machine +* 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 -* Improved Kubernetes resource installation handling -* Ensure that kernel arguments are applied during firstboot when kexec is used in ISO installations +* 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 +* Leftover combustion artifacts are now removed on first boot +* OS files and user provided certificates now maintain original permissions when copied to the final image * Dependency upgrades * "Phone Home" deployments are now utilizing Elemental v1.6 (upgraded from v1.4) * Embedded registry is now utilizing Hauler v1.0.7 (upgraded from v1.0.1) @@ -38,43 +24,24 @@ ### Image Definition Changes -* Introduced a dedicated FIPS mode option, adding the required packages, kernel arguments, and crypto selection - -## Bug Fixes - -* [#491](https://github.com/suse-edge/edge-image-builder/issues/491) - Large Helm manifests fail to install -* [#543](https://github.com/suse-edge/edge-image-builder/issues/543) - Kernel cmdline arguments aren't honoured in SL Micro 6.0 for SelfInstall ISO's -* [#550](https://github.com/suse-edge/edge-image-builder/issues/550) - PackageHub inclusion in RPM resolution silently errors on SLE Micro 6.0 - ---- - -# v1.1.0-rc1 - -## General - -* Added the ability to automatically copy files into the built images filesystem -* Kubernetes manifests are now applied in a systemd service -* Artifact sources origin and metadata are now extracted from a configuration file (`config/artifacts.yaml`) -* Dropped `-chart` suffix from installed Helm chart names -* Added ability to build aarch64 images on an aarch64 host machine -* Added caching for container images -* Added built image name output to build command -* Leftover combustion artifacts are now removed on first boot - -## API - -### Image Definition Changes - -* The `apiVersion` field now supports both `1.0` and `1.1` values +* The current version of the image definition has been incremented to `1.1` to include the changes below + * Existing definitions using the `1.0` version of the schema will continue to work with EIB +* Introduced a dedicated FIPS mode option (`enableFIPS`) which will enable FIPS mode on the node ### Image Configuration Directory Changes * 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 -* [#498](https://github.com/suse-edge/edge-image-builder/issues/498) - Fix kernelArgs issue with Leap Micro 6.0 * [#481](https://github.com/suse-edge/edge-image-builder/issues/481) - Certain Helm charts fail when templated without specified API Versions +* [#491](https://github.com/suse-edge/edge-image-builder/issues/491) - Large Helm manifests fail to install +* [#498](https://github.com/suse-edge/edge-image-builder/issues/498) - Fix kernelArgs issue with Leap Micro 6.0 +* [#543](https://github.com/suse-edge/edge-image-builder/issues/543) - Kernel cmdline arguments aren't honoured in SL Micro 6.0 for SelfInstall ISO's +* [#550](https://github.com/suse-edge/edge-image-builder/issues/550) - PackageHub inclusion in RPM resolution silently errors on SLE Micro 6.0 +* [#565](https://github.com/suse-edge/edge-image-builder/issues/565) - K3S SELinux uses an outdated package --- 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)