Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elemental validation and improvements when using SL Micro 6.0 #549

Merged
merged 10 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
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