diff --git a/e2e/nomostest/config_sync.go b/e2e/nomostest/config_sync.go index 58918a7803..a224987076 100644 --- a/e2e/nomostest/config_sync.go +++ b/e2e/nomostest/config_sync.go @@ -78,6 +78,28 @@ const ( shortSyncPollingPeriod = 5 * time.Second ) +// InstallMethod defines how Config Sync should be installed +type InstallMethod string + +const ( + // InstallMethodApply uses server-side apply (default) + InstallMethodApply InstallMethod = "apply" + // InstallMethodUpdate uses client-side update + InstallMethodUpdate InstallMethod = "update" +) + +// InstallConfigSyncOpts defines options for installing Config Sync +type InstallConfigSyncOpts struct { + Method InstallMethod +} + +// WithInstallMethod sets the installation method +func WithInstallMethod(method InstallMethod) func(*InstallConfigSyncOpts) { + return func(opts *InstallConfigSyncOpts) { + opts.Method = method + } +} + var ( // baseDir is the path to the Nomos repository root from test case files. // @@ -230,16 +252,55 @@ func parseConfigSyncManifests(nt *NT) ([]client.Object, error) { } // InstallConfigSync installs ConfigSync on the test cluster -func InstallConfigSync(nt *NT) error { - nt.T.Log("[SETUP] Installing Config Sync") +func InstallConfigSync(nt *NT, opts ...func(*InstallConfigSyncOpts)) error { + // Set default options + options := InstallConfigSyncOpts{ + Method: InstallMethodApply, // Default to apply method + } + + for _, opt := range opts { + opt(&options) + } + + nt.T.Log("[SETUP] Installing Config Sync using method: ", options.Method) objs, err := parseConfigSyncManifests(nt) if err != nil { return err } for _, o := range objs { nt.T.Logf("installConfigSync obj: %v", core.GKNN(o)) - if err := nt.KubeClient.Apply(o); err != nil { - return err + switch options.Method { + case InstallMethodApply: + if err := nt.KubeClient.Apply(o); err != nil { + return err + } + case InstallMethodUpdate: + // Create an empty Unstructured object for the Get operation + // We only need the name and namespace, not a full DeepCopy + currentObj := &unstructured.Unstructured{} + currentObj.SetName(o.GetName()) + currentObj.SetNamespace(o.GetNamespace()) + currentObj.SetGroupVersionKind(o.GetObjectKind().GroupVersionKind()) + if err := nt.KubeClient.Get(currentObj.GetName(), currentObj.GetNamespace(), currentObj); err != nil { + if apierrors.IsNotFound(err) { + if err := nt.KubeClient.Create(o); err != nil { + return err + } + } else { + return err + } + } else { + // Attach existing resourceVersion to the object for optimistic concurrency control. + // Kubernetes requires the resourceVersion to match the current cluster state for Update + // operations to prevent race conditions. Without this, the Update will fail with a conflict + // error since Kubernetes thinks we're trying to update an outdated version of the object. + // In this instance, the current state of the object on the cluster does not matter and the + // intent is to fully update to the original configuration declared in the manifest. + o.SetResourceVersion(currentObj.GetResourceVersion()) + if err := nt.KubeClient.Update(o); err != nil { + return err + } + } } } return nil diff --git a/e2e/testcases/cli_test.go b/e2e/testcases/cli_test.go index 854051e3e5..07fb5d1f0d 100644 --- a/e2e/testcases/cli_test.go +++ b/e2e/testcases/cli_test.go @@ -1297,13 +1297,14 @@ func TestApiResourceFormatting(t *testing.T) { } func TestNomosMigrate(t *testing.T) { - nt := nomostest.New(t, nomostesting.NomosCLI, ntopts.SkipConfigSyncInstall) + nt := nomostest.New(t, nomostesting.NomosCLI) nt.T.Cleanup(func() { // Restore state of Config Sync installation after test - if err := nomostest.InstallConfigSync(nt); err != nil { + if err := nomostest.InstallConfigSync(nt, nomostest.WithInstallMethod(nomostest.InstallMethodUpdate)); err != nil { nt.T.Fatal(err) } + nt.Must(nt.WatchForAllSyncs()) }) nt.T.Cleanup(func() { cmObj := &unstructured.Unstructured{ @@ -1451,11 +1452,11 @@ func TestNomosMigrate(t *testing.T) { configmanagement.RGControllerName, configmanagement.RGControllerNamespace) }) tg.Go(func() error { - return nt.Watcher.WatchForNotFound(kinds.Deployment(), + return nt.Watcher.WatchForCurrentStatus(kinds.Deployment(), core.RootReconcilerName(configsync.RootSyncName), configsync.ControllerNamespace) }) tg.Go(func() error { - return nt.Watcher.WatchForNotFound(kinds.RootSyncV1Beta1(), + return nt.Watcher.WatchForCurrentStatus(kinds.RootSyncV1Beta1(), configsync.RootSyncName, configsync.ControllerNamespace) }) if err := tg.Wait(); err != nil { @@ -1464,14 +1465,14 @@ func TestNomosMigrate(t *testing.T) { } func TestNomosMigrateMonoRepo(t *testing.T) { - nt := nomostest.New(t, nomostesting.NomosCLI, ntopts.SkipConfigSyncInstall) + nt := nomostest.New(t, nomostesting.NomosCLI) nt.T.Cleanup(func() { // Restore state of Config Sync installation after test. - // This also emulates upgrading to the current version after migrating - if err := nomostest.InstallConfigSync(nt); err != nil { + if err := nomostest.InstallConfigSync(nt, nomostest.WithInstallMethod(nomostest.InstallMethodUpdate)); err != nil { nt.T.Fatal(err) } + nt.Must(nt.WatchForAllSyncs()) }) nt.T.Cleanup(func() { crds := []string{ @@ -1707,13 +1708,14 @@ func TestNomosMigrateMonoRepo(t *testing.T) { // This test case validates the behavior of the uninstall script defined // at installation/uninstall_configmanagement.sh func TestACMUninstallScript(t *testing.T) { - nt := nomostest.New(t, nomostesting.NomosCLI, ntopts.SkipConfigSyncInstall) + nt := nomostest.New(t, nomostesting.NomosCLI) nt.T.Cleanup(func() { // Restore state of Config Sync installation after test - if err := nomostest.InstallConfigSync(nt); err != nil { + if err := nomostest.InstallConfigSync(nt, nomostest.WithInstallMethod(nomostest.InstallMethodUpdate)); err != nil { nt.T.Fatal(err) } + nt.Must(nt.WatchForAllSyncs()) }) nt.T.Cleanup(func() { cmObj := &unstructured.Unstructured{ @@ -1861,11 +1863,11 @@ func TestACMUninstallScript(t *testing.T) { configmanagement.RGControllerName, configmanagement.RGControllerNamespace) }) tg.Go(func() error { - return nt.Watcher.WatchForNotFound(kinds.Deployment(), + return nt.Watcher.WatchForCurrentStatus(kinds.Deployment(), core.RootReconcilerName(configsync.RootSyncName), configsync.ControllerNamespace) }) tg.Go(func() error { - return nt.Watcher.WatchForNotFound(kinds.RootSyncV1Beta1(), + return nt.Watcher.WatchForCurrentStatus(kinds.RootSyncV1Beta1(), configsync.RootSyncName, configsync.ControllerNamespace) }) if err := tg.Wait(); err != nil {