From ccc7fcdbfa702252c0fda929079de296e1a0f8fd Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 21 Jan 2025 13:51:28 -0800 Subject: [PATCH] feat(ec): migration from v1 to v2 - fixes (#1725) --- kinds/apis/v1beta1/installation_types.go | 4 +- .../controllers/installation_controller.go | 2 +- operator/pkg/charts/reconcile.go | 8 +- operator/pkg/cli/logging.go | 3 + operator/pkg/cli/migrate_v2_pod.go | 3 - operator/pkg/cli/migratev2/installation.go | 102 ++++- .../pkg/cli/migratev2/installation_test.go | 354 ++++++++---------- operator/pkg/cli/migratev2/migrate.go | 17 +- operator/pkg/cli/migratev2/operator.go | 37 +- operator/pkg/cli/migratev2/operator_test.go | 2 +- pkg/kubeutils/kubeutils.go | 2 + 11 files changed, 299 insertions(+), 235 deletions(-) diff --git a/kinds/apis/v1beta1/installation_types.go b/kinds/apis/v1beta1/installation_types.go index 5bf63f153..9402b1493 100644 --- a/kinds/apis/v1beta1/installation_types.go +++ b/kinds/apis/v1beta1/installation_types.go @@ -35,6 +35,7 @@ const ( InstallationStateInstalled string = "Installed" InstallationStateKubernetesInstalled string = "KubernetesInstalled" InstallationStateAddonsInstalling string = "AddonsInstalling" + InstallationStateAddonsInstalled string = "AddonsInstalled" InstallationStateHelmChartUpdateFailure string = "HelmChartUpdateFailure" InstallationStateObsolete string = "Obsolete" InstallationStateFailed string = "Failed" @@ -49,7 +50,8 @@ const ( ) const ( - DisableReconcileConditionType = "DisableReconcile" + ConditionTypeV2MigrationInProgress = "V2MigrationInProgress" + ConditionTypeDisableReconcile = "DisableReconcile" ) // ConfigSecretEntryName holds the entry name we are looking for in the secret diff --git a/operator/controllers/installation_controller.go b/operator/controllers/installation_controller.go index 91a335804..6f1507d10 100644 --- a/operator/controllers/installation_controller.go +++ b/operator/controllers/installation_controller.go @@ -590,7 +590,7 @@ func (r *InstallationReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } - if k8sutil.CheckConditionStatus(in.Status, v1beta1.DisableReconcileConditionType) == metav1.ConditionTrue { + if k8sutil.CheckConditionStatus(in.Status, v1beta1.ConditionTypeDisableReconcile) == metav1.ConditionTrue { log.Info("Installation reconciliation is disabled, reconciliation ended") return ctrl.Result{}, nil } diff --git a/operator/pkg/charts/reconcile.go b/operator/pkg/charts/reconcile.go index 2e1116187..d999a3017 100644 --- a/operator/pkg/charts/reconcile.go +++ b/operator/pkg/charts/reconcile.go @@ -9,8 +9,10 @@ import ( k0shelmv1beta1 "github.com/k0sproject/k0s/pkg/apis/helm/v1beta1" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/operator/pkg/k8sutil" "github.com/replicatedhq/embedded-cluster/operator/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/helpers" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -120,7 +122,11 @@ func ReconcileHelmCharts(ctx context.Context, cli client.Client, in *v1beta1.Ins if in.Status.State != v1beta1.InstallationStateInstalled { ev = &RecordedEvent{Reason: "AddonsUpgraded", Message: "Addons upgraded"} } - in.Status.SetState(v1beta1.InstallationStateInstalled, "Addons upgraded", nil) + if k8sutil.CheckConditionStatus(in.Status, v1beta1.ConditionTypeV2MigrationInProgress) == metav1.ConditionTrue { + in.Status.SetState(v1beta1.InstallationStateAddonsInstalled, "Addons upgraded", nil) + } else { + in.Status.SetState(v1beta1.InstallationStateInstalled, "Addons upgraded", nil) + } return ev, nil } diff --git a/operator/pkg/cli/logging.go b/operator/pkg/cli/logging.go index 5cc38f300..444b777e5 100644 --- a/operator/pkg/cli/logging.go +++ b/operator/pkg/cli/logging.go @@ -13,3 +13,6 @@ func NewLogger(level logrus.Level) (logr.Logger, error) { log := logrusr.New(logrusLog) return log, nil } + +// LogFunc can be used as an argument to functions that log messages. +type LogFunc func(string, ...any) diff --git a/operator/pkg/cli/migrate_v2_pod.go b/operator/pkg/cli/migrate_v2_pod.go index 89f41af7b..af2325efb 100644 --- a/operator/pkg/cli/migrate_v2_pod.go +++ b/operator/pkg/cli/migrate_v2_pod.go @@ -74,9 +74,6 @@ var _migrateV2PodSpec = corev1.Pod{ }, } -// LogFunc can be used as an argument to Run to log messages. -type LogFunc func(string, ...any) - // runMigrateV2PodAndWait runs the v2 migration pod and waits for the pod to finish. func runMigrateV2PodAndWait( ctx context.Context, logf LogFunc, cli client.Client, diff --git a/operator/pkg/cli/migratev2/installation.go b/operator/pkg/cli/migratev2/installation.go index 930dfda5a..599e7fd87 100644 --- a/operator/pkg/cli/migratev2/installation.go +++ b/operator/pkg/cli/migratev2/installation.go @@ -3,14 +3,72 @@ package migratev2 import ( "context" "fmt" + "time" + "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" ) +// setV2MigrationInProgress sets the Installation condition to indicate that the v2 migration is in +// progress. +func setV2MigrationInProgress(ctx context.Context, logf LogFunc, cli client.Client, in *ecv1beta1.Installation) error { + logf("Setting v2 migration in progress") + + err := setInstallationCondition(ctx, cli, in, metav1.Condition{ + Type: ecv1beta1.ConditionTypeV2MigrationInProgress, + Status: metav1.ConditionTrue, + Reason: "V2MigrationInProgress", + }) + if err != nil { + return fmt.Errorf("set v2 migration in progress condition: %w", err) + } + + logf("Successfully set v2 migration in progress") + return nil +} + +// waitForInstallationStateInstalled waits for the installation to be in a successful state and +// ready for the migration. +func waitForInstallationStateInstalled(ctx context.Context, logf LogFunc, cli client.Client, installation *ecv1beta1.Installation) error { + logf("Waiting for installation to reconcile") + + err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + in, err := kubeutils.GetCRDInstallation(ctx, cli, installation.Name) + if err != nil { + return false, fmt.Errorf("get installation: %w", err) + } + + switch in.Status.State { + // Success states + case ecv1beta1.InstallationStateInstalled, ecv1beta1.InstallationStateAddonsInstalled: + return true, nil + + // Failure states + case ecv1beta1.InstallationStateFailed, ecv1beta1.InstallationStateHelmChartUpdateFailure: + return false, fmt.Errorf("installation failed: %s", in.Status.Reason) + case ecv1beta1.InstallationStateObsolete: + return false, fmt.Errorf("installation is obsolete") + + // In progress states + default: + return false, nil + } + }) + if err != nil { + return err + } + + logf("Installation reconciled") + return nil +} + // copyInstallationsToConfigMaps copies the Installation CRs to ConfigMaps. func copyInstallationsToConfigMaps(ctx context.Context, logf LogFunc, cli client.Client) error { var installationList ecv1beta1.InstallationList @@ -37,7 +95,6 @@ func copyInstallationsToConfigMaps(ctx context.Context, logf LogFunc, cli client func ensureInstallationConfigMap(ctx context.Context, cli client.Client, in *ecv1beta1.Installation) error { copy := in.DeepCopy() - copy.Spec.SourceType = ecv1beta1.InstallationSourceTypeConfigMap err := kubeutils.CreateInstallation(ctx, cli, copy) if k8serrors.IsAlreadyExists(err) { err := kubeutils.UpdateInstallation(ctx, cli, copy) @@ -49,3 +106,46 @@ func ensureInstallationConfigMap(ctx context.Context, cli client.Client, in *ecv } return nil } + +// ensureInstallationStateInstalled sets the ConfigMap installation state to installed and updates +// the status to mark the upgrade as complete. +func ensureInstallationStateInstalled(ctx context.Context, logf LogFunc, cli client.Client, in *ecv1beta1.Installation) error { + logf("Setting installation state to installed") + + // the installation will be in a ConfigMap at this point + copy, err := kubeutils.GetInstallation(ctx, cli, in.Name) + if err != nil { + return fmt.Errorf("get installation: %w", err) + } + + copy.Status.SetState(v1beta1.InstallationStateInstalled, "V2MigrationComplete", nil) + meta.RemoveStatusCondition(©.Status.Conditions, ecv1beta1.ConditionTypeV2MigrationInProgress) + meta.RemoveStatusCondition(©.Status.Conditions, ecv1beta1.ConditionTypeDisableReconcile) + + err = kubeutils.UpdateInstallationStatus(ctx, cli, copy) + if err != nil { + return fmt.Errorf("update installation status: %w", err) + } + + logf("Successfully set installation state to installed") + return nil +} + +func setInstallationCondition(ctx context.Context, cli client.Client, in *ecv1beta1.Installation, condition metav1.Condition) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var copy ecv1beta1.Installation + err := cli.Get(ctx, client.ObjectKey{Name: in.Name}, ©) + if err != nil { + return fmt.Errorf("get installation: %w", err) + } + + copy.Status.SetCondition(condition) + + err = cli.Status().Update(ctx, ©) + if err != nil { + return fmt.Errorf("update installation status: %w", err) + } + + return nil + }) +} diff --git a/operator/pkg/cli/migratev2/installation_test.go b/operator/pkg/cli/migratev2/installation_test.go index 9d2642c40..d67c27622 100644 --- a/operator/pkg/cli/migratev2/installation_test.go +++ b/operator/pkg/cli/migratev2/installation_test.go @@ -3,265 +3,217 @@ package migratev2 import ( "context" "testing" + "time" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func Test_copyInstallationsToConfigMaps(t *testing.T) { +func Test_waitForInstallationStateInstalled(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, ecv1beta1.AddToScheme(scheme)) + tests := []struct { - name string - installs []ecv1beta1.Installation - existingConfigs []corev1.ConfigMap - expectError bool - validate func(*testing.T, client.Client) + name string + installation *ecv1beta1.Installation + updateFunc func(*ecv1beta1.Installation) // Function to update installation state during test + expectError bool + errorString string }{ { - name: "copies single installation to configmap when none exists", - installs: []ecv1beta1.Installation{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-install", - }, - Spec: ecv1beta1.InstallationSpec{ - Config: &ecv1beta1.ConfigSpec{ - Version: "1.0.0", - }, - }, + name: "installation already in installed state", + installation: &ecv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", + }, + Status: ecv1beta1.InstallationStatus{ + State: ecv1beta1.InstallationStateInstalled, }, }, expectError: false, - validate: func(t *testing.T, cli client.Client) { - var cm corev1.ConfigMap - err := cli.Get(context.Background(), types.NamespacedName{ - Namespace: "embedded-cluster", - Name: "test-install", - }, &cm) - require.NoError(t, err) - - // Verify configmap has correct labels - assert.Equal(t, "embedded-cluster", cm.Labels["replicated.com/installation"]) - assert.Equal(t, "ec-install", cm.Labels["replicated.com/disaster-recovery"]) - - // Verify installation data is present - assert.Contains(t, cm.Data["installation"], `"version":"1.0.0"`) - }, }, { - name: "updates existing configmap with new installation data", - installs: []ecv1beta1.Installation{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-install", - }, - Spec: ecv1beta1.InstallationSpec{ - Config: &ecv1beta1.ConfigSpec{ - Version: "2.0.0", // New version - }, - }, + name: "installation already in failed state", + installation: &ecv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", }, - }, - existingConfigs: []corev1.ConfigMap{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-install", - Namespace: "embedded-cluster", - Labels: map[string]string{ - "replicated.com/installation": "embedded-cluster", - "replicated.com/disaster-recovery": "ec-install", - }, - }, - Data: map[string]string{ - "installation": `{"spec":{"config":{"version":"1.0.0"}}}`, // Old version - }, + Status: ecv1beta1.InstallationStatus{ + State: ecv1beta1.InstallationStateFailed, + Reason: "something went wrong", }, }, - expectError: false, - validate: func(t *testing.T, cli client.Client) { - var cm corev1.ConfigMap - err := cli.Get(context.Background(), types.NamespacedName{ - Namespace: "embedded-cluster", - Name: "test-install", - }, &cm) - require.NoError(t, err) - - // Verify configmap has correct labels - assert.Equal(t, "embedded-cluster", cm.Labels["replicated.com/installation"]) - assert.Equal(t, "ec-install", cm.Labels["replicated.com/disaster-recovery"]) - - // Verify installation data is updated - assert.Contains(t, cm.Data["installation"], `"version":"2.0.0"`) - assert.NotContains(t, cm.Data["installation"], `"version":"1.0.0"`) - }, + expectError: true, + errorString: "installation failed: something went wrong", }, { - name: "handles mix of new and existing configmaps", - installs: []ecv1beta1.Installation{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "install-1", - }, - Spec: ecv1beta1.InstallationSpec{ - Config: &ecv1beta1.ConfigSpec{ - Version: "2.0.0", // Updated version - }, - }, + name: "installation transitions to installed state", + installation: &ecv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "install-2", - }, - Spec: ecv1beta1.InstallationSpec{ - Config: &ecv1beta1.ConfigSpec{ - Version: "1.0.0", // New installation - }, - }, + Status: ecv1beta1.InstallationStatus{ + State: ecv1beta1.InstallationStateAddonsInstalling, }, }, - existingConfigs: []corev1.ConfigMap{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "install-1", - Namespace: "embedded-cluster", - Labels: map[string]string{ - "replicated.com/installation": "embedded-cluster", - "replicated.com/disaster-recovery": "ec-install", - }, - }, - Data: map[string]string{ - "installation": `{"spec":{"config":{"version":"1.0.0"}}}`, // Old version - }, - }, + updateFunc: func(in *ecv1beta1.Installation) { + in.Status.State = ecv1beta1.InstallationStateInstalled }, expectError: false, - validate: func(t *testing.T, cli client.Client) { - var cms corev1.ConfigMapList - err := cli.List(context.Background(), &cms, client.InNamespace("embedded-cluster")) - require.NoError(t, err) - assert.Len(t, cms.Items, 2) - - // Verify both configmaps exist with correct data - for _, cm := range cms.Items { - assert.Equal(t, "embedded-cluster", cm.Labels["replicated.com/installation"]) - assert.Equal(t, "ec-install", cm.Labels["replicated.com/disaster-recovery"]) - if cm.Name == "install-1" { - assert.Contains(t, cm.Data["installation"], `"version":"2.0.0"`) - assert.NotContains(t, cm.Data["installation"], `"version":"1.0.0"`) - } else { - assert.Contains(t, cm.Data["installation"], `"version":"1.0.0"`) - } - } - }, }, { - name: "copies multiple installations to configmaps", - installs: []ecv1beta1.Installation{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "install-1", - }, - Spec: ecv1beta1.InstallationSpec{ - Config: &ecv1beta1.ConfigSpec{ - Version: "1.0.0", - }, - }, + name: "installation transitions to failed state", + installation: &ecv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "install-2", - }, - Spec: ecv1beta1.InstallationSpec{ - Config: &ecv1beta1.ConfigSpec{ - Version: "2.0.0", - }, - }, + Status: ecv1beta1.InstallationStatus{ + State: ecv1beta1.InstallationStateAddonsInstalling, }, }, - expectError: false, - validate: func(t *testing.T, cli client.Client) { - var cms corev1.ConfigMapList - err := cli.List(context.Background(), &cms, client.InNamespace("embedded-cluster")) - require.NoError(t, err) - assert.Len(t, cms.Items, 2) - - // Verify both configmaps exist with correct data - for _, cm := range cms.Items { - assert.Equal(t, "embedded-cluster", cm.Labels["replicated.com/installation"]) - assert.Equal(t, "ec-install", cm.Labels["replicated.com/disaster-recovery"]) - if cm.Name == "install-1" { - assert.Contains(t, cm.Data["installation"], `"version":"1.0.0"`) - } else { - assert.Contains(t, cm.Data["installation"], `"version":"2.0.0"`) - } - } + updateFunc: func(in *ecv1beta1.Installation) { + in.Status.State = ecv1beta1.InstallationStateHelmChartUpdateFailure + in.Status.Reason = "helm chart update failed" }, + expectError: true, + errorString: "installation failed: helm chart update failed", }, { - name: "handles no installations", - installs: []ecv1beta1.Installation{}, - expectError: false, - validate: func(t *testing.T, cli client.Client) { - var cms corev1.ConfigMapList - err := cli.List(context.Background(), &cms, client.InNamespace("embedded-cluster")) - require.NoError(t, err) - assert.Empty(t, cms.Items) + name: "installation becomes obsolete", + installation: &ecv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", + }, + Status: ecv1beta1.InstallationStatus{ + State: ecv1beta1.InstallationStateAddonsInstalling, + }, + }, + updateFunc: func(in *ecv1beta1.Installation) { + in.Status.State = ecv1beta1.InstallationStateObsolete + in.Status.Reason = "This is not the most recent installation object" }, + expectError: true, + errorString: "installation is obsolete", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, ecv1beta1.AddToScheme(scheme)) - require.NoError(t, corev1.AddToScheme(scheme)) - - // Create fake client with installations and existing configmaps + // Create a fake client with the test installation cli := fake.NewClientBuilder(). WithScheme(scheme). - WithObjects(append( - installationsToRuntimeObjects(tt.installs), - configMapsToRuntimeObjects(tt.existingConfigs)..., - )...). + WithObjects(tt.installation). + WithStatusSubresource(tt.installation). Build() - logf := func(format string, args ...any) { - // No-op logger for testing + // If there's an update function, run it in a goroutine after a short delay + if tt.updateFunc != nil { + go func() { + time.Sleep(100 * time.Millisecond) + var installation ecv1beta1.Installation + err := cli.Get(context.Background(), client.ObjectKey{Name: tt.installation.Name}, &installation) + require.NoError(t, err) + tt.updateFunc(&installation) + err = cli.Status().Update(context.Background(), &installation) + require.NoError(t, err) + }() } - // Run the function - err := copyInstallationsToConfigMaps(context.Background(), logf, cli) + // Call waitForInstallationStateInstalled + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err := waitForInstallationStateInstalled(ctx, t.Logf, cli, tt.installation) if tt.expectError { require.Error(t, err) - return + if tt.errorString != "" { + assert.Contains(t, err.Error(), tt.errorString) + } + } else { + require.NoError(t, err) } - require.NoError(t, err) - - // Run validation - tt.validate(t, cli) }) } } -func installationsToRuntimeObjects(installs []ecv1beta1.Installation) []client.Object { - objects := make([]client.Object, len(installs)) - for i := range installs { - objects[i] = &installs[i] +func Test_ensureInstallationStateInstalled(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, ecv1beta1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + + tests := []struct { + name string + installation *ecv1beta1.Installation + expectError bool + }{ + { + name: "updates installation state and removes conditions", + installation: &ecv1beta1.Installation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", + }, + Status: ecv1beta1.InstallationStatus{ + State: ecv1beta1.InstallationStateAddonsInstalled, + Conditions: []metav1.Condition{ + { + Type: ecv1beta1.ConditionTypeV2MigrationInProgress, + Status: metav1.ConditionTrue, + Reason: "V2MigrationInProgress", + ObservedGeneration: 1, + }, + { + Type: ecv1beta1.ConditionTypeDisableReconcile, + Status: metav1.ConditionTrue, + Reason: "V2MigrationInProgress", + ObservedGeneration: 1, + }, + }, + }, + }, + expectError: false, + }, } - return objects -} -func configMapsToRuntimeObjects(configs []corev1.ConfigMap) []client.Object { - objects := make([]client.Object, len(configs)) - for i := range configs { - objects[i] = &configs[i] + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a fake client with the test installation + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(tt.installation). + WithStatusSubresource(tt.installation). + Build() + + // Call ensureInstallationStateInstalled + err := ensureInstallationStateInstalled(context.Background(), t.Logf, cli, tt.installation) + + if tt.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + + // Verify the installation was updated correctly + var updatedInstallation ecv1beta1.Installation + err = cli.Get(context.Background(), client.ObjectKey{Name: tt.installation.Name}, &updatedInstallation) + require.NoError(t, err) + + // Check state is set to Installed + assert.Equal(t, ecv1beta1.InstallationStateInstalled, updatedInstallation.Status.State) + assert.Equal(t, "V2MigrationComplete", updatedInstallation.Status.Reason) + + // Check conditions are removed + condition := meta.FindStatusCondition(updatedInstallation.Status.Conditions, ecv1beta1.ConditionTypeV2MigrationInProgress) + assert.Nil(t, condition, "V2MigrationInProgress condition should be removed") + + condition = meta.FindStatusCondition(updatedInstallation.Status.Conditions, ecv1beta1.ConditionTypeDisableReconcile) + assert.Nil(t, condition, "DisableReconcile condition should be removed") + }) } - return objects } diff --git a/operator/pkg/cli/migratev2/migrate.go b/operator/pkg/cli/migratev2/migrate.go index fe4cc6c13..2b02f4339 100644 --- a/operator/pkg/cli/migratev2/migrate.go +++ b/operator/pkg/cli/migratev2/migrate.go @@ -20,7 +20,17 @@ func Run( in *ecv1beta1.Installation, migrationSecret string, appSlug string, appVersionLabel string, ) error { - err := runManagerInstallPodsAndWait(ctx, logf, cli, in, migrationSecret, appSlug, appVersionLabel) + err := setV2MigrationInProgress(ctx, logf, cli, in) + if err != nil { + return fmt.Errorf("set v2 migration in progress: %w", err) + } + + err = waitForInstallationStateInstalled(ctx, logf, cli, in) + if err != nil { + return fmt.Errorf("failed to wait for addon installation: %w", err) + } + + err = runManagerInstallPodsAndWait(ctx, logf, cli, in, migrationSecret, appSlug, appVersionLabel) if err != nil { return fmt.Errorf("run manager install pods: %w", err) } @@ -46,6 +56,11 @@ func Run( return fmt.Errorf("enable v2 admin console: %w", err) } + err = ensureInstallationStateInstalled(ctx, logf, cli, in) + if err != nil { + return fmt.Errorf("set installation state to installed: %w", err) + } + err = cleanupV1(ctx, logf, cli) if err != nil { return fmt.Errorf("cleanup v1: %w", err) diff --git a/operator/pkg/cli/migratev2/operator.go b/operator/pkg/cli/migratev2/operator.go index 16cf52669..60b9afc80 100644 --- a/operator/pkg/cli/migratev2/operator.go +++ b/operator/pkg/cli/migratev2/operator.go @@ -10,40 +10,27 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apitypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) +// disableOperator sets the DisablingReconcile condition to true on the installation object which +// will prevent the operator from reconciling the installation. func disableOperator(ctx context.Context, logf LogFunc, cli client.Client, in *ecv1beta1.Installation) error { logf("Disabling operator") - for timer := time.NewTimer(0); ; timer = time.NewTimer(2 * time.Second) { - <-timer.C - - var copy ecv1beta1.Installation - err := cli.Get(ctx, client.ObjectKey{Name: in.Name}, ©) - if err != nil { - return fmt.Errorf("get installation: %w", err) - } - - copy.Status.SetCondition(metav1.Condition{ - Type: ecv1beta1.DisableReconcileConditionType, - Status: metav1.ConditionTrue, - Reason: "V2MigrationInProgress", - }) - - err = cli.Status().Update(ctx, ©) - if k8serrors.IsConflict(err) { - continue - } else if err != nil { - return fmt.Errorf("update installation status: %w", err) - } - - logf("Successfully disabled operator") - return nil + err := setInstallationCondition(ctx, cli, in, metav1.Condition{ + Type: ecv1beta1.ConditionTypeDisableReconcile, + Status: metav1.ConditionTrue, + Reason: "V2MigrationInProgress", + }) + if err != nil { + return fmt.Errorf("set disable reconcile condition: %w", err) } + + logf("Successfully disabled operator") + return nil } // cleanupV1 removes control of the Helm Charts from the k0s controller and uninstalls the Embedded diff --git a/operator/pkg/cli/migratev2/operator_test.go b/operator/pkg/cli/migratev2/operator_test.go index 6df4b7a8c..b00edf773 100644 --- a/operator/pkg/cli/migratev2/operator_test.go +++ b/operator/pkg/cli/migratev2/operator_test.go @@ -62,7 +62,7 @@ func Test_disableOperator(t *testing.T) { require.NoError(t, err) // Check that the DisableReconcile condition was set correctly - condition := meta.FindStatusCondition(updatedInstallation.Status.Conditions, ecv1beta1.DisableReconcileConditionType) + condition := meta.FindStatusCondition(updatedInstallation.Status.Conditions, ecv1beta1.ConditionTypeDisableReconcile) require.NotNil(t, condition) assert.Equal(t, metav1.ConditionTrue, condition.Status) assert.Equal(t, "V2MigrationInProgress", condition.Reason) diff --git a/pkg/kubeutils/kubeutils.go b/pkg/kubeutils/kubeutils.go index 570989d68..8ae14e5a2 100644 --- a/pkg/kubeutils/kubeutils.go +++ b/pkg/kubeutils/kubeutils.go @@ -228,6 +228,8 @@ func (k *KubeUtils) WaitForInstallation(ctx context.Context, cli client.Client, } func CreateInstallation(ctx context.Context, cli client.Client, in *ecv1beta1.Installation) error { + in.Spec.SourceType = ecv1beta1.InstallationSourceTypeConfigMap + data, err := json.Marshal(in) if err != nil { return fmt.Errorf("marshal installation: %w", err)