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
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* 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
* Improved Elemental handling when using SL Micro 6.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rephrase this and move it to the API section below? Elemental configuration now requires a registration code in order to install the necessary RPMs from the official sources or something similar.

* Added Elemental configuration validation

## API

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
21 changes: 13 additions & 8 deletions pkg/eib/eib.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,19 @@ func appendElementalRPMs(ctx *image.Context) {

log.AuditInfo("Elemental registration is configured. The necessary RPM packages will be downloaded.")

appendRPMs(ctx, []image.AddRepo{
{
URL: ctx.ArtifactSources.Elemental.RegisterRepository,
},
{
URL: ctx.ArtifactSources.Elemental.SystemAgentRepository,
},
}, combustion.ElementalPackages...)
if ctx.ImageDefinition.APIVersion == "1.0" {
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
appendRPMs(ctx, []image.AddRepo{
{
URL: ctx.ArtifactSources.Elemental.RegisterRepository,
},
{
URL: ctx.ArtifactSources.Elemental.SystemAgentRepository,
},
}, combustion.ElementalPackages...)
} else if ctx.ImageDefinition.APIVersion == "1.1" {
appendRPMs(ctx, nil, combustion.ElementalPackages...)
}

}

func appendFips(ctx *image.Context) {
Expand Down
68 changes: 68 additions & 0 deletions pkg/image/validation/elemental.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package validation

import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/suse-edge/edge-image-builder/pkg/image"
"go.uber.org/zap"
)

const (
elementalComponent = "Elemental"
)

func validateElemental(ctx *image.Context) []FailedValidation {
var failures []FailedValidation

elementalConfigDir := filepath.Join(ctx.ImageConfigDir, "elemental")
failures = append(failures, validateElementalDir(elementalConfigDir)...)

if ctx.ImageDefinition.APIVersion == "1.1" && 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 {
if errors.Is(err, os.ErrNotExist) {
return nil
}

zap.S().Errorf("Elemental config directory could not be read: %s", err)
failures = append(failures, FailedValidation{
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
UserMessage: fmt.Sprintf("Elemental config directory could not be read: %s", err),
dbw7 marked this conversation as resolved.
Show resolved Hide resolved
})
}

if len(elementalConfigDirEntries) == 0 {
failures = append(failures, FailedValidation{
UserMessage: "Elemental config directory should not be present if it is empty",
})
}

if len(elementalConfigDirEntries) > 1 {
failures = append(failures, FailedValidation{
UserMessage: "Elemental config directory should only contain a singular 'elemental_config.yaml' file",
})
}

if len(elementalConfigDirEntries) == 1 {
if elementalConfigDirEntries[0].Name() != "elemental_config.yaml" {
failures = append(failures, FailedValidation{
UserMessage: "Elemental config file should only be named `elemental_config.yaml`",
})
}
}

return failures
}
175 changes: 175 additions & 0 deletions pkg/image/validation/elemental_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package validation

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/suse-edge/edge-image-builder/pkg/image"
)

func TestValidateElemental(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 1.1`: {
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
ImageDefinition: &image.Definition{
APIVersion: "1.1",
OperatingSystem: image.OperatingSystem{
Packages: image.Packages{
RegCode: "registration-code",
},
},
},
},
`1.1 no registration code`: {
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
ImageDefinition: &image.Definition{
APIVersion: "1.1",
},
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 TestValidateElementalConfigDir(t *testing.T) {
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
configDirValid, err := os.MkdirTemp("", "eib-config-")
require.NoError(t, err)

configDirEmpty, err := os.MkdirTemp("", "eib-config-")
require.NoError(t, err)

configDirMultipleFiles, err := os.MkdirTemp("", "eib-config-")
require.NoError(t, err)

configDirInvalidName, err := os.MkdirTemp("", "eib-config-")
require.NoError(t, err)

configDirUnreadable, err := os.MkdirTemp("", "eib-config-")
require.NoError(t, err)

defer func() {
assert.NoError(t, os.RemoveAll(configDirValid))
assert.NoError(t, os.RemoveAll(configDirEmpty))
assert.NoError(t, os.RemoveAll(configDirMultipleFiles))
assert.NoError(t, os.RemoveAll(configDirInvalidName))
assert.NoError(t, os.RemoveAll(configDirUnreadable))
}()

elementalDirValid := filepath.Join(configDirValid, "elemental")
require.NoError(t, os.MkdirAll(elementalDirValid, os.ModePerm))

validElementalConfig := filepath.Join(elementalDirValid, "elemental_config.yaml")
require.NoError(t, os.WriteFile(validElementalConfig, []byte(""), 0o600))

elementalDirEmpty := filepath.Join(configDirEmpty, "elemental")
require.NoError(t, os.MkdirAll(elementalDirEmpty, os.ModePerm))

elementalDirMultipleFiles := filepath.Join(configDirMultipleFiles, "elemental")
require.NoError(t, os.MkdirAll(elementalDirMultipleFiles, os.ModePerm))

firstElementalConfig := filepath.Join(elementalDirMultipleFiles, "elemental_config1.yaml")
require.NoError(t, os.WriteFile(firstElementalConfig, []byte(""), 0o600))
secondElementalConfig := filepath.Join(elementalDirMultipleFiles, "elemental_config2.yaml")
require.NoError(t, os.WriteFile(secondElementalConfig, []byte(""), 0o600))

elementalDirInvalidName := filepath.Join(configDirInvalidName, "elemental")
require.NoError(t, os.MkdirAll(elementalDirInvalidName, os.ModePerm))

invalidElementalConfig := filepath.Join(elementalDirInvalidName, "elemental.yaml")
require.NoError(t, os.WriteFile(invalidElementalConfig, []byte(""), 0o600))

elementalDirUnreadable := filepath.Join(configDirUnreadable, "elemental")
require.NoError(t, os.MkdirAll(elementalDirUnreadable, os.ModePerm))
require.NoError(t, os.Chmod(elementalDirUnreadable, 0o333))

tests := map[string]struct {
ExpectedFailedMessages []string
ElementalDir string
}{
`valid elemental dir`: {
ElementalDir: elementalDirValid,
},
`empty elemental dir`: {
ElementalDir: elementalDirEmpty,
ExpectedFailedMessages: []string{
"Elemental config directory should not be present if it is empty",
},
},
`multiple files in elemental dir`: {
ElementalDir: elementalDirMultipleFiles,
ExpectedFailedMessages: []string{
"Elemental config directory should only contain a singular 'elemental_config.yaml' file",
},
},
`invalid name in elemental dir`: {
ElementalDir: elementalDirInvalidName,
ExpectedFailedMessages: []string{
"Elemental config file should only be named `elemental_config.yaml`",
},
},
`unreadable elemental dir`: {
ElementalDir: elementalDirUnreadable,
ExpectedFailedMessages: []string{
fmt.Sprintf("Elemental config directory could not be read: open %s: permission denied", elementalDirUnreadable),
"Elemental config directory should not be present if it is empty",
},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
failures := validateElementalDir(test.ElementalDir)
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)
}

})
}
}
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