Skip to content

Commit

Permalink
elemental validation and SL Micro 6.0 handling improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
dbw7 committed Aug 28, 2024
1 parent a80991f commit 3f35603
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 12 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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
* Added Elemental configuration validation

## API

Expand Down
21 changes: 13 additions & 8 deletions pkg/eib/eib.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,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" {
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 appendRPMs(ctx *image.Context, repos []image.AddRepo, packages ...string) {
Expand Down
67 changes: 67 additions & 0 deletions pkg/image/validation/elemental.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package validation

import (
"errors"
"fmt"
"github.com/suse-edge/edge-image-builder/pkg/image"
"go.uber.org/zap"
"os"
"path/filepath"
)

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{
UserMessage: fmt.Sprintf("Elemental config directory could not be read: %s", err),
})
}

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
}
174 changes: 174 additions & 0 deletions pkg/image/validation/elemental_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package validation

import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/suse-edge/edge-image-builder/pkg/image"
"os"
"path/filepath"
"testing"
)

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`: {
ImageDefinition: image.Definition{
APIVersion: "1.1",
OperatingSystem: image.OperatingSystem{
Packages: image.Packages{
RegCode: "registration-code",
},
},
},
},
`1.1 no registration code`: {
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) {
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, 0333))

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

})
}
}
9 changes: 5 additions & 4 deletions pkg/image/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ func ValidateDefinition(ctx *image.Context) map[string][]FailedValidation {

validations := map[string]validateComponent{
versionComponent: validateVersion,
imageComponent: validateImage,
osComponent: validateOperatingSystem,
registryComponent: validateEmbeddedArtifactRegistry,
k8sComponent: validateKubernetes,
imageComponent: validateImage,
osComponent: validateOperatingSystem,
registryComponent: validateEmbeddedArtifactRegistry,
k8sComponent: validateKubernetes,
elementalComponent: validateElemental,
}
for componentName, v := range validations {
componentFailures := v(ctx)
Expand Down

0 comments on commit 3f35603

Please sign in to comment.