Repository: suse-edge/edge-image-builder (go)

Issue:
`enableFIPs` Fails Without Registration Code
When FIPS is enabled in the image definition without a registration code, the build fails with
```
Loading repository data...
Warning: No repositories defined. Operating only with the installed resolvables. Nothing can be installed.
Reading installed packages...
Package 'patterns-base-fips' not found.
```
Providing a registration code allows the build to be successful or an additional repository containing the package allows it to be successful.

Pull request:
Add FIPS Validation
Closes #659 

Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 1a6eb1c..76a85a1 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -6,6 +6,7 @@
 
 * Added single-stack IPv6 and dual-stack networking support for Kubernetes
 * SUSEConnect now properly activates the SL Micro "Extras" module
+* Improved Validation for `operatingSystem.enableFIPS` flag
 * Improved Embedded Artifact Registry handling to no longer be memory bound
 * Dependency upgrades
   * Go module version is now upgraded from `1.22` to `1.24`  
@@ -17,7 +18,7 @@
 ### Image Definition Changes
 
 * Added `kubernetes.network.apiVIP6` field to enable cluster LoadBalancer based on IPv6 address
-* Added the `enableExtras` flag to enable the SUSE Linux Extras repository during RPM resolution.
+* Added the `operatingSystem.enableExtras` flag to enable the SUSE Linux Extras repository during RPM resolution.
 
 ### Image Configuration Directory Changes
 
diff --git a/pkg/combustion/combustion.go b/pkg/combustion/combustion.go
index dcb2724..6c83a1e 100644
--- a/pkg/combustion/combustion.go
+++ b/pkg/combustion/combustion.go
@@ -130,7 +130,7 @@ func (c *Combustion) Configure(ctx *image.Context) error {
 		},
 		{
 			name:     fipsComponentName,
-			runnable: configureFips,
+			runnable: configureFIPS,
 		},
 		{
 			name:     elementalComponentName,
diff --git a/pkg/combustion/fips.go b/pkg/combustion/fips.go
index 5e9f50e..1445d28 100644
--- a/pkg/combustion/fips.go
+++ b/pkg/combustion/fips.go
@@ -19,18 +19,18 @@ const (
 var (
 	//go:embed templates/15-fips-setup.sh
 	fipsScript     string
-	FipsPackages   = []string{"patterns-base-fips"}
-	FipsKernelArgs = []string{"fips=1"}
+	FIPSPackages   = []string{"patterns-base-fips"}
+	FIPSKernelArgs = []string{"fips=1"}
 )
 
-func configureFips(ctx *image.Context) ([]string, error) {
-	fips := ctx.ImageDefinition.OperatingSystem.EnableFips
+func configureFIPS(ctx *image.Context) ([]string, error) {
+	fips := ctx.ImageDefinition.OperatingSystem.EnableFIPS
 	if !fips {
 		log.AuditComponentSkipped(fipsComponentName)
 		return nil, nil
 	}
 
-	if err := writeFipsCombustionScript(ctx); err != nil {
+	if err := writeFIPSCombustionScript(ctx); err != nil {
 		log.AuditComponentFailed(fipsComponentName)
 		return nil, err
 	}
@@ -39,7 +39,7 @@ func configureFips(ctx *image.Context) ([]string, error) {
 	return []string{fipsScriptName}, nil
 }
 
-func writeFipsCombustionScript(ctx *image.Context) error {
+func writeFIPSCombustionScript(ctx *image.Context) error {
 	fipsScriptFilename := filepath.Join(ctx.CombustionDir, fipsScriptName)
 
 	if err := os.WriteFile(fipsScriptFilename, []byte(fipsScript), fileio.ExecutablePerms); err != nil {
diff --git a/pkg/eib/eib.go b/pkg/eib/eib.go
index 3c06c23..63f669d 100644
--- a/pkg/eib/eib.go
+++ b/pkg/eib/eib.go
@@ -32,7 +32,7 @@ func Run(ctx *image.Context, rootBuildDir string) error {
 	}
 
 	appendElementalRPMs(ctx)
-	appendFips(ctx)
+	appendFIPS(ctx)
 	appendHelm(ctx)
 
 	c, err := buildCombustion(ctx, rootBuildDir)
@@ -112,12 +112,19 @@ func appendElementalRPMs(ctx *image.Context) {
 	}
 }
 
-func appendFips(ctx *image.Context) {
-	fips := ctx.ImageDefinition.OperatingSystem.EnableFips
+func appendFIPS(ctx *image.Context) {
+	fips := ctx.ImageDefinition.OperatingSystem.EnableFIPS
 	if fips {
 		log.AuditInfo("FIPS mode is configured. The necessary RPM packages will be downloaded.")
-		appendRPMs(ctx, nil, combustion.FipsPackages...)
-		appendKernelArgs(ctx, combustion.FipsKernelArgs...)
+
+		packages := ctx.ImageDefinition.OperatingSystem.Packages
+		if packages.RegCode == "" && len(packages.AdditionalRepos) > 0 {
+			log.Audit("WARNING: FIPS enabled with no SUSE registration code provided. Package resolution may fail if additional repositories do not contain the `patterns-base-fips` package.")
+			zap.S().Warn("Detected FIPS for installation with no sccRegistrationCode provided")
+		}
+
+		appendRPMs(ctx, nil, combustion.FIPSPackages...)
+		appendKernelArgs(ctx, combustion.FIPSKernelArgs...)
 	}
 }
 
diff --git a/pkg/image/definition.go b/pkg/image/definition.go
index 0107ce1..3944203 100644
--- a/pkg/image/definition.go
+++ b/pkg/image/definition.go
@@ -76,7 +76,7 @@ type OperatingSystem struct {
 	Time             Time                   `yaml:"time"`
 	Proxy            Proxy                  `yaml:"proxy"`
 	Keymap           string                 `yaml:"keymap"`
-	EnableFips       bool                   `yaml:"enableFIPS"`
+	EnableFIPS       bool                   `yaml:"enableFIPS"`
 }
 
 type IsoConfiguration struct {
diff --git a/pkg/image/validation/os.go b/pkg/image/validation/os.go
index 7cc3590..1c2a09e 100644
--- a/pkg/image/validation/os.go
+++ b/pkg/image/validation/os.go
@@ -24,6 +24,7 @@ func validateOperatingSystem(ctx *image.Context) []FailedValidation {
 	failures = append(failures, validateSuma(&def.OperatingSystem)...)
 	failures = append(failures, validatePackages(&def.OperatingSystem)...)
 	failures = append(failures, validateTimeSync(&def.OperatingSystem)...)
+	failures = append(failures, validateFIPS(&def.OperatingSystem)...)
 	failures = append(failures, validateIsoConfig(def)...)
 	failures = append(failures, validateRawConfig(def)...)
 
@@ -47,7 +48,7 @@ func validateKernelArgs(os *image.OperatingSystem) []FailedValidation {
 				})
 			}
 
-			if (key == "fips" && value == "1") && !os.EnableFips {
+			if (key == "fips" && value == "1") && !os.EnableFIPS {
 				failures = append(failures, FailedValidation{
 					UserMessage: "FIPS mode has been specified via kernel arguments, please use the 'enableFIPS: true' option instead.",
 				})
@@ -289,3 +290,20 @@ func validateTimeSync(os *image.OperatingSystem) []FailedValidation {
 
 	return failures
 }
+
+func validateFIPS(os *image.OperatingSystem) []FailedValidation {
+	var failures []FailedValidation
+
+	if !os.EnableFIPS {
+		return nil
+	}
+
+	if os.Packages.RegCode == "" && len(os.Packages.AdditionalRepos) == 0 {
+		msg := "To enable FIPS you must either provide an SCC registration code or link an additional repository that contains the `patterns-base-fips` package."
+		failures = append(failures, FailedValidation{
+			UserMessage: msg,
+		})
+	}
+
+	return failures
+}
diff --git a/pkg/image/validation/version.go b/pkg/image/validation/version.go
index ea2a39f..c7ef680 100644
--- a/pkg/image/validation/version.go
+++ b/pkg/image/validation/version.go
@@ -27,7 +27,7 @@ func validateVersion(ctx *image.Context) []FailedValidation {
 		})
 	}
 
-	if definition.APIVersion == "1.0" && definition.OperatingSystem.EnableFips {
+	if definition.APIVersion == "1.0" && definition.OperatingSystem.EnableFIPS {
 		failures = append(failures, FailedValidation{
 			UserMessage: "Automated FIPS configuration is not supported in EIB version 1.0, please use EIB version >= 1.1",
 		})


Test patch:
diff --git a/pkg/combustion/fips_test.go b/pkg/combustion/fips_test.go
index 964bf26..d011e71 100644
--- a/pkg/combustion/fips_test.go
+++ b/pkg/combustion/fips_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/suse-edge/edge-image-builder/pkg/image"
 )
 
-func TestConfigureFips_NoConf(t *testing.T) {
+func TestConfigureFIPS_NoConf(t *testing.T) {
 	// Setup
 	var ctx image.Context
 
@@ -20,26 +20,26 @@ func TestConfigureFips_NoConf(t *testing.T) {
 	}
 
 	// Test
-	scripts, err := configureFips(&ctx)
+	scripts, err := configureFIPS(&ctx)
 
 	// Verify
 	require.NoError(t, err)
 	assert.Nil(t, scripts)
 }
 
-func TestConfigureFips_Enabled(t *testing.T) {
+func TestConfigureFIPS_Enabled(t *testing.T) {
 	// Setup
 	ctx, teardown := setupContext(t)
 	defer teardown()
 
 	ctx.ImageDefinition = &image.Definition{
 		OperatingSystem: image.OperatingSystem{
-			EnableFips: true,
+			EnableFIPS: true,
 		},
 	}
 
 	// Test
-	scripts, err := configureFips(ctx)
+	scripts, err := configureFIPS(ctx)
 
 	// Verify
 	require.NoError(t, err)
diff --git a/pkg/image/definition_test.go b/pkg/image/definition_test.go
index 7cb88cd..bdfbb90 100644
--- a/pkg/image/definition_test.go
+++ b/pkg/image/definition_test.go
@@ -38,7 +38,7 @@ func TestParse(t *testing.T) {
 	assert.Equal(t, expectedKernelArgs, definition.OperatingSystem.KernelArgs)
 
 	// Operating System -> FIPS
-	enableFIPS := definition.OperatingSystem.EnableFips
+	enableFIPS := definition.OperatingSystem.EnableFIPS
 	assert.Equal(t, true, enableFIPS)
 
 	// Operating System -> Groups
diff --git a/pkg/image/validation/os_test.go b/pkg/image/validation/os_test.go
index 09ae3c7..5003146 100644
--- a/pkg/image/validation/os_test.go
+++ b/pkg/image/validation/os_test.go
@@ -778,3 +778,61 @@ func TestValidateTimeSync(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateFIPS(t *testing.T) {
+	tests := map[string]struct {
+		OperatingSystem        image.OperatingSystem
+		ExpectedFailedMessages []string
+	}{
+		`not included`: {
+			OperatingSystem: image.OperatingSystem{
+				EnableFIPS: false,
+			},
+		},
+		`FIPS enabled no SCC code or additional repo`: {
+			OperatingSystem: image.OperatingSystem{
+				EnableFIPS: true,
+			},
+			ExpectedFailedMessages: []string{
+				"To enable FIPS you must either provide an SCC registration code or link an additional repository that contains the `patterns-base-fips` package.",
+			},
+		},
+		`FIPS enabled with SCC code`: {
+			OperatingSystem: image.OperatingSystem{
+				EnableFIPS: true,
+				Packages: image.Packages{
+					RegCode: "scc-code",
+				},
+			},
+		},
+		`FIPS enabled with additional repos`: {
+			OperatingSystem: image.OperatingSystem{
+				EnableFIPS: true,
+				Packages: image.Packages{
+					AdditionalRepos: []image.AddRepo{
+						{
+							URL: "https://additional-repo.suse",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			os := test.OperatingSystem
+			failures := validateFIPS(&os)
+			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)
+			}
+		})
+	}
+}
diff --git a/pkg/image/validation/version_test.go b/pkg/image/validation/version_test.go
index c9ff338..5a94f1b 100644
--- a/pkg/image/validation/version_test.go
+++ b/pkg/image/validation/version_test.go
@@ -39,7 +39,7 @@ func TestValidateVersion(t *testing.T) {
 			ImageDefinition: image.Definition{
 				APIVersion: "1.0",
 				OperatingSystem: image.OperatingSystem{
-					EnableFips: true,
+					EnableFIPS: true,
 				},
 			},
 			ExpectedFailedMessages: []string{