Skip to content

Commit

Permalink
Elemental validation and improvements when using SL Micro 6.0 (#549)
Browse files Browse the repository at this point in the history
* elemental validation and SL Micro 6.0 handling improvements

* add note to docs

* fixes for linter

* feedback updates

* Update pkg/image/validation/elemental.go

Co-authored-by: Atanas Dinov <[email protected]>

* Update pkg/image/validation/elemental.go

Co-authored-by: Atanas Dinov <[email protected]>

* feedback updates

* last changes

---------

Co-authored-by: Atanas Dinov <[email protected]>
  • Loading branch information
dbw7 and atanasdinov authored Sep 24, 2024
1 parent f9b21bc commit d0a8af1
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 20 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 0 additions & 3 deletions config/artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/building-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 1 addition & 8 deletions pkg/eib/eib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 0 additions & 4 deletions pkg/image/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
74 changes: 74 additions & 0 deletions pkg/image/validation/elemental.go
Original file line number Diff line number Diff line change
@@ -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
}
150 changes: 150 additions & 0 deletions pkg/image/validation/elemental_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
11 changes: 6 additions & 5 deletions pkg/image/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit d0a8af1

Please sign in to comment.