Skip to content

Commit

Permalink
Merge branch 'main' into rke2-ha
Browse files Browse the repository at this point in the history
  • Loading branch information
atanasdinov authored Jan 22, 2024
2 parents 9149e66 + 0d94d12 commit 97fc5c2
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 70 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ podman run --rm -it \
volume to make the build artifacts available after the container completes. In this example, a directory named
`_build` will be created in the image configuration directory and will persist after EIB finishes. This directory
will contain subdirectories storing the respective artifacts of the different builds.
* `-validate` - If specified, the specified image definition and configuration directory will be checked to ensure
the build can proceed, however the image will not actually be built.

## Testing Images

Expand Down
45 changes: 40 additions & 5 deletions cmd/eib/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import (
"log"
"os"
"path/filepath"
"slices"
"strings"

"github.com/suse-edge/edge-image-builder/pkg/build"
"github.com/suse-edge/edge-image-builder/pkg/combustion"
"github.com/suse-edge/edge-image-builder/pkg/image"
"github.com/suse-edge/edge-image-builder/pkg/image/validation"
"github.com/suse-edge/edge-image-builder/pkg/kubernetes"
audit "github.com/suse-edge/edge-image-builder/pkg/log"
"github.com/suse-edge/edge-image-builder/pkg/network"
Expand All @@ -24,18 +27,21 @@ const (
argConfigFile = "config-file"
argConfigDir = "config-dir"
argBuildDir = "build-dir"
argValidate = "validate"
)

func processArgs() (*image.Context, error) {
var (
configFile string
configDir string
rootBuildDir string
validate bool
)

flag.StringVar(&configFile, argConfigFile, "", "name of the image configuration file")
flag.StringVar(&configDir, argConfigDir, "", "full path to the image configuration directory")
flag.StringVar(&rootBuildDir, argBuildDir, "", "full path to the directory to store build artifacts")
flag.BoolVar(&validate, argValidate, false, "if specified, the image definition will be validated but not built")
flag.Parse()

imageDefinition, err := parseImageDefinition(configFile, configDir)
Expand Down Expand Up @@ -66,6 +72,40 @@ func processArgs() (*image.Context, error) {
KubernetesArtefactDownloader: kubernetes.ArtefactDownloader{},
}

failedValidations := validation.ValidateDefinition(ctx)
if len(failedValidations) > 0 {
audit.Audit("Image definition validation found the following errors:")

logMessageBuilder := strings.Builder{}

orderedComponentNames := make([]string, 0, len(failedValidations))
for c := range failedValidations {
orderedComponentNames = append(orderedComponentNames, c)
}
slices.Sort(orderedComponentNames)

for _, componentName := range orderedComponentNames {
failures := failedValidations[componentName]
audit.Audit(fmt.Sprintf(" %s", componentName))
for _, cf := range failures {
audit.Audit(fmt.Sprintf(" %s", cf.UserMessage))
logMessageBuilder.WriteString(cf.UserMessage + "\n")
if cf.Error != nil {
logMessageBuilder.WriteString("\t" + cf.Error.Error() + "\n")
}
}
}

if s := logMessageBuilder.String(); s != "" {
zap.S().Fatalf("Image definition validation failures:\n%s", s)
}
}

if validate {
audit.Audit("The specified image definition is valid.")
os.Exit(0)
}

if !combustion.SkipRPMComponent(ctx) {
p, err := podman.New(buildDir)
if err != nil {
Expand Down Expand Up @@ -109,11 +149,6 @@ func parseImageDefinition(configFile string, configDir string) (*image.Definitio
return nil, fmt.Errorf("error parsing definition file \"%s\": %w", configFile, err)
}

err = image.ValidateDefinition(imageDefinition)
if err != nil {
return nil, fmt.Errorf("error validating definition file: %w", err)
}

return imageDefinition, nil
}

Expand Down
10 changes: 1 addition & 9 deletions pkg/image/validation/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

const (
imageComponent = "image"
imageComponent = "Image"
)

func validateImage(ctx *image.Context) []FailedValidation {
Expand All @@ -25,40 +25,34 @@ func validateImage(ctx *image.Context) []FailedValidation {
if def.Image.ImageType == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'imageType' field is required in the 'image' section.",
Component: imageComponent,
})
} else if !slices.Contains(validImageTypes, def.Image.ImageType) {
msg := fmt.Sprintf("The 'imageType' field must be one of: %s", strings.Join(validImageTypes, ", "))
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: imageComponent,
})
}

if def.Image.Arch == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'arch' field is required in the 'image' section.",
Component: imageComponent,
})
} else if !slices.Contains(validArchTypes, string(def.Image.Arch)) {
msg := fmt.Sprintf("The 'arch' field must be one of: %s", strings.Join(validArchTypes, ", "))
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: imageComponent,
})
}

if def.Image.OutputImageName == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'outputImageName' field is required in the 'image' section.",
Component: imageComponent,
})
}

if def.Image.BaseImage == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'baseImage' field is required in the 'image' section.",
Component: imageComponent,
})
} else {
baseImageFilename := filepath.Join(ctx.ImageConfigDir, "images", def.Image.BaseImage)
Expand All @@ -68,13 +62,11 @@ func validateImage(ctx *image.Context) []FailedValidation {
msg := fmt.Sprintf("The specified base image '%s' cannot be found.", def.Image.BaseImage)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: imageComponent,
})
} else {
msg := fmt.Sprintf("The specified base image '%s' cannot be read. See the logs for more information.", def.Image.BaseImage)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: imageComponent,
Error: err,
})
}
Expand Down
1 change: 0 additions & 1 deletion pkg/image/validation/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ func TestValidateImage(t *testing.T) {
var foundMessages []string
for _, foundValidation := range failedValidations {
foundMessages = append(foundMessages, foundValidation.UserMessage)
assert.Equal(t, imageComponent, foundValidation.Component)
}

for _, expectedMessage := range test.ExpectedFailedMessages {
Expand Down
10 changes: 0 additions & 10 deletions pkg/image/validation/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,12 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation {
if k8s.Network.APIVIP == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.",
Component: k8sComponent,
})
}

if k8s.Network.APIHost == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'apiHost' field is required in the 'network' section when defining entries under 'nodes'.",
Component: k8sComponent,
})
}

Expand All @@ -64,7 +62,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation {
if node.Hostname == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'hostname' field is required for entries in the 'nodes' section.",
Component: k8sComponent,
})
}

Expand All @@ -73,7 +70,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation {
msg := fmt.Sprintf("The 'type' field for entries in the 'nodes' section must be one of: %s", options)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: k8sComponent,
})
}

Expand All @@ -85,7 +81,6 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation {
msg := fmt.Sprintf("The node labeled with 'initialiser' must be of type '%s'.", image.KubernetesNodeTypeServer)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: k8sComponent,
})
}
}
Expand All @@ -99,22 +94,19 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation {
msg := fmt.Sprintf("The 'nodes' section contains duplicate entries: %s", duplicateValues)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: k8sComponent,
})
}

if !slices.Contains(nodeTypes, image.KubernetesNodeTypeServer) {
msg := fmt.Sprintf("There must be at least one node of type '%s' defined.", image.KubernetesNodeTypeServer)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: k8sComponent,
})
}

if len(initialisers) > 1 {
failures = append(failures, FailedValidation{
UserMessage: "Only one node may be specified as the cluster initializer.",
Component: k8sComponent,
})
}

Expand All @@ -133,15 +125,13 @@ func validateManifestURLs(k8s *image.Kubernetes) []FailedValidation {
if !strings.HasPrefix(manifest, "http") {
failures = append(failures, FailedValidation{
UserMessage: "Entries in 'urls' must begin with either 'http://' or 'https://'.",
Component: k8sComponent,
})
}

if _, exists := seenManifests[manifest]; exists {
msg := fmt.Sprintf("The 'urls' field contains duplicate entries: %s", manifest)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: k8sComponent,
})
}

Expand Down
3 changes: 0 additions & 3 deletions pkg/image/validation/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ func TestValidateKubernetes(t *testing.T) {
var foundMessages []string
for _, foundValidation := range failures {
foundMessages = append(foundMessages, foundValidation.UserMessage)
assert.Equal(t, k8sComponent, foundValidation.Component)
}

for _, expectedMessage := range test.ExpectedFailedMessages {
Expand Down Expand Up @@ -295,7 +294,6 @@ func TestValidateNodes(t *testing.T) {
var foundMessages []string
for _, foundValidation := range failures {
foundMessages = append(foundMessages, foundValidation.UserMessage)
assert.Equal(t, k8sComponent, foundValidation.Component)
}

for _, expectedMessage := range test.ExpectedFailedMessages {
Expand Down Expand Up @@ -369,7 +367,6 @@ func TestValidateManifestURLs(t *testing.T) {
var foundMessages []string
for _, foundValidation := range failures {
foundMessages = append(foundMessages, foundValidation.UserMessage)
assert.Equal(t, k8sComponent, foundValidation.Component)
}

for _, expectedMessage := range test.ExpectedFailedMessages {
Expand Down
16 changes: 0 additions & 16 deletions pkg/image/validation/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,13 @@ func validateKernelArgs(os *image.OperatingSystem) []FailedValidation {
if key == "" || value == "" {
failures = append(failures, FailedValidation{
UserMessage: "Kernel arguments must be specified as 'key=value'.",
Component: osComponent,
})
}
}

if _, exists := seenKeys[key]; exists {
failures = append(failures, FailedValidation{
UserMessage: fmt.Sprintf("Duplicate kernel argument found: %s", key),
Component: osComponent,
})
}
seenKeys[key] = true
Expand All @@ -77,7 +75,6 @@ func validateSystemd(os *image.OperatingSystem) []FailedValidation {
msg := fmt.Sprintf("Systemd enable list contains duplicate entries: %s", duplicateValues)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

Expand All @@ -86,7 +83,6 @@ func validateSystemd(os *image.OperatingSystem) []FailedValidation {
msg := fmt.Sprintf("Systemd disable list contains duplicate entries: %s", duplicateValues)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

Expand All @@ -96,7 +92,6 @@ func validateSystemd(os *image.OperatingSystem) []FailedValidation {
msg := fmt.Sprintf("Systemd conflict found, '%s' is both enabled and disabled.", enableItem)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}
}
Expand All @@ -113,23 +108,20 @@ func validateUsers(os *image.OperatingSystem) []FailedValidation {
if user.Username == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'username' field is required for all entries under 'users'.",
Component: osComponent,
})
}

if user.EncryptedPassword == "" && user.SSHKey == "" {
msg := fmt.Sprintf("User '%s' must have either a password or SSH key.", user.Username)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

if seenUsernames[user.Username] {
msg := fmt.Sprintf("Duplicate username found: %s", user.Username)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}
seenUsernames[user.Username] = true
Expand All @@ -147,19 +139,16 @@ func validateSuma(os *image.OperatingSystem) []FailedValidation {
if os.Suma.Host == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'host' field is required for the 'suma' section.",
Component: osComponent,
})
}
if strings.HasPrefix(os.Suma.Host, "http") {
failures = append(failures, FailedValidation{
UserMessage: "The suma 'host' field may not contain 'http://' or 'https://'",
Component: osComponent,
})
}
if os.Suma.ActivationKey == "" {
failures = append(failures, FailedValidation{
UserMessage: "The 'activationKey' field is required for the 'suma' section.",
Component: osComponent,
})
}

Expand All @@ -174,7 +163,6 @@ func validatePackages(os *image.OperatingSystem) []FailedValidation {
msg := fmt.Sprintf("The 'packageList' field contains duplicate packages: %s", duplicateValues)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

Expand All @@ -183,14 +171,12 @@ func validatePackages(os *image.OperatingSystem) []FailedValidation {
msg := fmt.Sprintf("The 'additionalRepos' field contains duplicate repos: %s", duplicateValues)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

if len(os.Packages.PKGList) > 0 && len(os.Packages.AdditionalRepos) == 0 && os.Packages.RegCode == "" {
failures = append(failures, FailedValidation{
UserMessage: "When including the 'packageList' field, either additional repositories or a registration code must be included.",
Component: osComponent,
})
}

Expand All @@ -204,15 +190,13 @@ func validateUnattended(def *image.Definition) []FailedValidation {
msg := fmt.Sprintf("The 'unattended' field can only be used when 'imageType' is '%s'.", image.TypeISO)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

if def.Image.ImageType != image.TypeISO && def.OperatingSystem.InstallDevice != "" {
msg := fmt.Sprintf("The 'installDevice' field can only be used when 'imageType' is '%s'.", image.TypeISO)
failures = append(failures, FailedValidation{
UserMessage: msg,
Component: osComponent,
})
}

Expand Down
Loading

0 comments on commit 97fc5c2

Please sign in to comment.