diff --git a/pkg/daemon/update.go b/pkg/daemon/update.go index 2bab88331f..691efc4fe4 100644 --- a/pkg/daemon/update.go +++ b/pkg/daemon/update.go @@ -1683,7 +1683,69 @@ func (dn *CoreOSDaemon) applyExtensions(oldConfig, newConfig *mcfgv1.MachineConf // Add "update" to the start of argument list args = append([]string{constants.RPMOSTreeUpdateArg}, args...) logSystem("Applying extensions : %+q", args) - return runRpmOstree(args...) + if err := runRpmOstree(args...); err != nil { + return err + } + + // Verify that the extensions were staged successfully + if err := dn.verifyExtensionsInStagedDeployment(newConfig); err != nil { + return fmt.Errorf("extension verification failed: %w", err) + } + + return nil +} + +// verifyExtensionsInStagedDeployment verifies that all required extension packages +// are present in the staged deployment after rpm-ostree has applied the changes. +// Assisted by: Cursor +func (dn *Daemon) verifyExtensionsInStagedDeployment(newConfig *mcfgv1.MachineConfig) error { + // Get the staged deployment + _, staged, err := dn.NodeUpdaterClient.GetBootedAndStagedDeployment() + if err != nil { + return fmt.Errorf("failed to get staged deployment: %w", err) + } + if staged == nil { + return fmt.Errorf("no staged deployment found after applying extensions") + } + + // Get the required packages for the new config's extensions + requiredPackages, err := ctrlcommon.GetPackagesForSupportedExtensions(newConfig.Spec.Extensions) + if err != nil { + return fmt.Errorf("failed to get required packages for extensions: %w", err) + } + + // This is just a dummy addition to the expected package list to simulate a seemingly + // successful extension install that is actually missing a package to test the degrade. + requiredPackages = append(requiredPackages, "dummy-rpm") + // Verify the extension packages have installed + if err := verifyExtensionPackagesInDeployment(requiredPackages, staged.RequestedPackages); err != nil { + return err + } + + logSystem("Extension verification successful: all required packages are staged") + return nil +} + +// verifyExtensionPackagesInDeployment checks that all required packages are present +// in the deployment's requested packages list. +// Assisted by: Cursor +func verifyExtensionPackagesInDeployment(requiredPackages []string, deploymentPackages []string) error { + // Build a set of packages in the deployment + deploymentPackageSet := sets.New(deploymentPackages...) + + // Check that all required packages are present + missingPackages := []string{} + for _, pkg := range requiredPackages { + if !deploymentPackageSet.Has(pkg) { + missingPackages = append(missingPackages, pkg) + } + } + + if len(missingPackages) > 0 { + return fmt.Errorf("extensions not staged correctly, missing packages: %v", missingPackages) + } + + return nil } // switchKernel updates kernel on host with the kernelType specified in MachineConfig. diff --git a/pkg/daemon/update_test.go b/pkg/daemon/update_test.go index 40228b95b9..e59d55ea0f 100644 --- a/pkg/daemon/update_test.go +++ b/pkg/daemon/update_test.go @@ -857,6 +857,97 @@ func TestFindClosestFilePolicyPathMatch(t *testing.T) { } } +// Assisted by: Cursor +func TestVerifyExtensionPackagesInDeployment(t *testing.T) { + tests := []struct { + name string + requiredPackages []string + deploymentPackages []string + expectError bool + errorContains string + }{ + { + name: "no required packages, no deployment packages", + requiredPackages: []string{}, + deploymentPackages: []string{}, + expectError: false, + }, + { + name: "no required packages, some deployment packages", + requiredPackages: []string{}, + deploymentPackages: []string{"some-package", "another-package"}, + expectError: false, + }, + { + name: "all required packages present", + requiredPackages: []string{"crun-wasm"}, + deploymentPackages: []string{"crun-wasm", "other-package"}, + expectError: false, + }, + { + name: "multiple required packages all present", + requiredPackages: []string{"NetworkManager-libreswan", "libreswan"}, + deploymentPackages: []string{"NetworkManager-libreswan", "libreswan", "other-package"}, + expectError: false, + }, + { + name: "missing single required package", + requiredPackages: []string{"crun-wasm"}, + deploymentPackages: []string{"other-package"}, + expectError: true, + errorContains: "crun-wasm", + }, + { + name: "missing multiple required packages", + requiredPackages: []string{"NetworkManager-libreswan", "libreswan"}, + deploymentPackages: []string{"other-package"}, + expectError: true, + errorContains: "NetworkManager-libreswan", + }, + { + name: "partial packages present - missing one", + requiredPackages: []string{"NetworkManager-libreswan", "libreswan"}, + deploymentPackages: []string{"NetworkManager-libreswan", "other-package"}, + expectError: true, + errorContains: "libreswan", + }, + { + name: "two-node-ha extension packages all present", + requiredPackages: []string{"pacemaker", "pcs", "fence-agents-all"}, + deploymentPackages: []string{"pacemaker", "pcs", "fence-agents-all"}, + expectError: false, + }, + { + name: "two-node-ha extension packages partially present", + requiredPackages: []string{"pacemaker", "pcs", "fence-agents-all"}, + deploymentPackages: []string{"pacemaker", "pcs"}, + expectError: true, + errorContains: "fence-agents-all", + }, + { + name: "required packages present with extra packages in deployment", + requiredPackages: []string{"usbguard"}, + deploymentPackages: []string{"usbguard", "random-package-1", "random-package-2"}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := verifyExtensionPackagesInDeployment(tt.requiredPackages, tt.deploymentPackages) + + if tt.expectError { + assert.Error(t, err, "expected error but got none") + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains, "error message should contain expected text") + } + } else { + assert.NoError(t, err, "unexpected error") + } + }) + } +} + func TestGenerateExtensionsArgs(t *testing.T) { tests := []struct { name string