From 58f5db334e118be62c475968af20b36ea5cea29c Mon Sep 17 00:00:00 2001 From: Andreas Gerstmayr Date: Fri, 26 May 2023 12:14:22 +0200 Subject: [PATCH] Implement operator upgrade Resolves: #296 Signed-off-by: Andreas Gerstmayr --- .chloggen/upgrade.yaml | 16 ++ apis/tempo/v1alpha1/tempostack_types.go | 6 + cmd/start/main.go | 33 +++- .../bases/tempo.grafana.com_tempostacks.yaml | 3 + go.mod | 1 + go.sum | 2 + internal/status/status.go | 3 + internal/status/status_test.go | 1 + internal/upgrade/suite_test.go | 54 +++++++ internal/upgrade/upgrade.go | 138 +++++++++++++++++ internal/upgrade/upgrade_test.go | 146 ++++++++++++++++++ internal/upgrade/v0_1_0.go | 9 ++ internal/upgrade/versions.go | 24 +++ 13 files changed, 429 insertions(+), 7 deletions(-) create mode 100644 .chloggen/upgrade.yaml create mode 100644 internal/upgrade/suite_test.go create mode 100644 internal/upgrade/upgrade.go create mode 100644 internal/upgrade/upgrade_test.go create mode 100644 internal/upgrade/v0_1_0.go create mode 100644 internal/upgrade/versions.go diff --git a/.chloggen/upgrade.yaml b/.chloggen/upgrade.yaml new file mode 100644 index 000000000..df248db5c --- /dev/null +++ b/.chloggen/upgrade.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. operator, target allocator, github action) +component: operator + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Implement operator upgrade + +# One or more tracking issues related to the change +issues: [296] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/apis/tempo/v1alpha1/tempostack_types.go b/apis/tempo/v1alpha1/tempostack_types.go index 52063ffec..db8b3b1a7 100644 --- a/apis/tempo/v1alpha1/tempostack_types.go +++ b/apis/tempo/v1alpha1/tempostack_types.go @@ -196,12 +196,18 @@ type ComponentStatus struct { // TempoStackStatus defines the observed state of TempoStack. type TempoStackStatus struct { + // Version of the Tempo Operator. + // +optional + OperatorVersion string `json:"operatorVersion,omitempty"` + // Version of the managed Tempo instance. // +optional TempoVersion string `json:"tempoVersion,omitempty"` + // Version of the Tempo Query component used. // +optional TempoQueryVersion string `json:"tempoQueryVersion,omitempty"` + // Components provides summary of all Tempo pod status grouped // per component. // diff --git a/cmd/start/main.go b/cmd/start/main.go index 59a179953..f54f19205 100644 --- a/cmd/start/main.go +++ b/cmd/start/main.go @@ -1,6 +1,7 @@ package start import ( + "context" "os" "runtime" @@ -11,10 +12,12 @@ import ( "github.com/spf13/cobra" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/manager" tempov1alpha1 "github.com/os-observability/tempo-operator/apis/tempo/v1alpha1" "github.com/os-observability/tempo-operator/cmd" controllers "github.com/os-observability/tempo-operator/controllers/tempo" + "github.com/os-observability/tempo-operator/internal/upgrade" "github.com/os-observability/tempo-operator/internal/version" //+kubebuilder:scaffold:imports ) @@ -23,6 +26,7 @@ func start(c *cobra.Command, args []string) { rootCmdConfig := c.Context().Value(cmd.RootConfigKey{}).(cmd.RootConfig) ctrlConfig, options := rootCmdConfig.CtrlConfig, rootCmdConfig.Options setupLog := ctrl.Log.WithName("setup") + version := version.Get() mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) if err != nil { @@ -30,6 +34,22 @@ func start(c *cobra.Command, args []string) { os.Exit(1) } + // run the upgrade mechanism once the manager is ready + err = mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { + upgrade := &upgrade.Upgrade{ + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("tempo-upgrade"), + CtrlConfig: ctrlConfig, + Version: version, + Log: ctrl.Log.WithName("UpgradeTask"), + } + return upgrade.TempoStacks(ctx) + })) + if err != nil { + setupLog.Error(err, "failed to upgrade TempoStack instances") + os.Exit(1) + } + if ctrlConfig.Gates.BuiltInCertManagement.Enabled { if err = (&controllers.CertRotationReconciler{ Client: mgr.GetClient(), @@ -67,17 +87,16 @@ func start(c *cobra.Command, args []string) { os.Exit(1) } - v := version.Get() setupLog.Info("Starting Tempo Operator", - "build-date", v.BuildDate, - "revision", v.Revision, - "tempo-operator", v.OperatorVersion, - "tempo", v.TempoVersion, - "tempo-query", v.TempoQueryVersion, + "build-date", version.BuildDate, + "revision", version.Revision, + "tempo-operator", version.OperatorVersion, + "tempo", version.TempoVersion, + "tempo-query", version.TempoQueryVersion, "default-tempo-image", rootCmdConfig.CtrlConfig.DefaultImages.Tempo, "default-tempo-query-image", rootCmdConfig.CtrlConfig.DefaultImages.TempoQuery, "default-tempo-gateway-image", rootCmdConfig.CtrlConfig.DefaultImages.TempoGateway, - "go-version", v.GoVersion, + "go-version", version.GoVersion, "go-arch", runtime.GOARCH, "go-os", runtime.GOOS, ) diff --git a/config/crd/bases/tempo.grafana.com_tempostacks.yaml b/config/crd/bases/tempo.grafana.com_tempostacks.yaml index 637560206..6107bffb3 100644 --- a/config/crd/bases/tempo.grafana.com_tempostacks.yaml +++ b/config/crd/bases/tempo.grafana.com_tempostacks.yaml @@ -1001,6 +1001,9 @@ spec: - type type: object type: array + operatorVersion: + description: Version of the Tempo Operator. + type: string tempoQueryVersion: description: Version of the Tempo Query component used. type: string diff --git a/go.mod b/go.mod index 99150b2c0..8f2ce7018 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/go.sum b/go.sum index 26428b922..64575566c 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ViaQ/logerr/v2 v2.1.0 h1:8WwzuNa1x+a6tRUl+6sFel83A/QxlFBUaFW2FyG2zzY= github.com/ViaQ/logerr/v2 v2.1.0/go.mod h1:/qoWLm3YG40Sv5u75s4fvzjZ5p36xINzaxU2L+DJ9uw= diff --git a/internal/status/status.go b/internal/status/status.go index 712360b19..7ee4d5a89 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -29,6 +29,9 @@ func Refresh(ctx context.Context, k StatusClient, tempo v1alpha1.TempoStack, sta // The .status.version field is empty for new CRs and cannot be set in the Defaulter webhook. // The upgrade procedure only runs once at operator startup, therefore we need to set // the initial status field versions here. + if status.OperatorVersion == "" { + changed.Status.OperatorVersion = version.Get().OperatorVersion + } if status.TempoVersion == "" { changed.Status.TempoVersion = version.Get().TempoVersion } diff --git a/internal/status/status_test.go b/internal/status/status_test.go index 0411e6f09..1fb80f940 100644 --- a/internal/status/status_test.go +++ b/internal/status/status_test.go @@ -55,6 +55,7 @@ func TestRefreshNoError(t *testing.T) { } s := v1alpha1.TempoStackStatus{ + OperatorVersion: "0.1.0", TempoVersion: "2.0", TempoQueryVersion: "main-1b50ad3", Conditions: ReadyCondition(c, stack), diff --git a/internal/upgrade/suite_test.go b/internal/upgrade/suite_test.go new file mode 100644 index 000000000..08920f5b5 --- /dev/null +++ b/internal/upgrade/suite_test.go @@ -0,0 +1,54 @@ +package upgrade + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/os-observability/tempo-operator/apis/tempo/v1alpha1" + //+kubebuilder:scaffold:imports +) + +var k8sClient client.Client +var testEnv *envtest.Environment +var testScheme *runtime.Scheme = scheme.Scheme + +func TestMain(m *testing.M) { + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + //WebhookInstallOptions: envtest.WebhookInstallOptions{ + // Paths: []string{filepath.Join("..", "config", "webhook")}, + //}, + } + cfg, err := testEnv.Start() + if err != nil { + fmt.Printf("failed to start testEnv: %v", err) + os.Exit(1) + } + + if err := v1alpha1.AddToScheme(testScheme); err != nil { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + if err != nil { + fmt.Printf("failed to setup a Kubernetes client: %v", err) + os.Exit(1) + } + + code := m.Run() + err = testEnv.Stop() + if err != nil { + fmt.Printf("failed to stop testEnv: %v", err) + os.Exit(1) + } + + os.Exit(code) +} diff --git a/internal/upgrade/upgrade.go b/internal/upgrade/upgrade.go new file mode 100644 index 000000000..121f28e40 --- /dev/null +++ b/internal/upgrade/upgrade.go @@ -0,0 +1,138 @@ +// The upgrade process in this package is based on opentelemetry-operator's upgrade process, +// licensed under the Apache License, Version 2.0. +// https://github.com/open-telemetry/opentelemetry-operator/tree/0a92a119f5acdcd775169e946638217ff5c78a1d/pkg/collector/upgrade +package upgrade + +import ( + "context" + "fmt" + "reflect" + + "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" + configv1alpha1 "github.com/os-observability/tempo-operator/apis/config/v1alpha1" + "github.com/os-observability/tempo-operator/apis/tempo/v1alpha1" + "github.com/os-observability/tempo-operator/internal/version" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Upgrade struct { + Client client.Client + Recorder record.EventRecorder + CtrlConfig configv1alpha1.ProjectConfig + Version version.Version + Log logr.Logger +} + +// TempoStacks upgrades all TempoStacks in the cluster +func (u Upgrade) TempoStacks(ctx context.Context) error { + u.Log.Info("looking for instances to upgrade") + + listOps := []client.ListOption{ + client.MatchingLabels(map[string]string{ + "app.kubernetes.io/managed-by": "tempo-operator", + }), + } + tempostackList := &v1alpha1.TempoStackList{} + if err := u.Client.List(ctx, tempostackList, listOps...); err != nil { + return fmt.Errorf("failed to list TempoStacks: %w", err) + } + + for i := range tempostackList.Items { + original := tempostackList.Items[i] + itemLogger := u.Log.WithValues("name", original.Name, "namespace", original.Namespace) + + upgraded, err := u.TempoStack(ctx, original) + if err != nil { + msg := "automated upgrade is not possible, the CR instance must be corrected and re-created manually" + itemLogger.Info(msg) + u.Recorder.Event(&original, "Error", "Upgrade", msg) + continue + } + + // only save if there were changes to the CR + if !reflect.DeepEqual(upgraded, tempostackList.Items[i]) { + // the resource update overrides the status, so, keep it so that we can reset it later + status := upgraded.Status + patch := client.MergeFrom(&original) + if err := u.Client.Patch(ctx, &upgraded, patch); err != nil { + itemLogger.Error(err, "failed to apply changes to instance") + continue + } + + // the status object requires its own update + upgraded.Status = status + if err := u.Client.Status().Patch(ctx, &upgraded, patch); err != nil { + itemLogger.Error(err, "failed to apply changes to instance's status object") + continue + } + + itemLogger.Info("upgraded instance", "from_version", tempostackList.Items[i].Status.OperatorVersion, "to_version", upgraded.Status.OperatorVersion) + } + } + + if len(tempostackList.Items) == 0 { + u.Log.Info("no instances to upgrade") + } + + return nil +} + +// TempoStack upgrades a single TempoStack CR to the latest known version. +// It runs all upgrade procedures between the current and latest operator version. +// Note: It does not save/apply the changes to the CR. +func (u Upgrade) TempoStack(ctx context.Context, tempo v1alpha1.TempoStack) (v1alpha1.TempoStack, error) { + log := u.Log.WithValues("namespace", tempo.Namespace, "tempo", tempo.Name) + + instanceVersion, err := semver.NewVersion(tempo.Status.OperatorVersion) + if err != nil { + log.Error(err, "failed to parse TempoStack operator version", "version", tempo.Status.OperatorVersion) + return tempo, err + } + + operatorVersion, err := semver.NewVersion(u.Version.OperatorVersion) + if err != nil { + u.Log.Error(err, "failed to parse current operator version", "version", u.Version.OperatorVersion) + return tempo, err + } + + if tempo.Status.OperatorVersion == u.Version.OperatorVersion { + log.Info("instance is already up-to-date", "version", tempo.Status.OperatorVersion) + return tempo, nil + } + + if instanceVersion.GreaterThan(operatorVersion) { + log.Info("skipping upgrading this instance because it's newer than the current running operator version", "version", tempo.Status.OperatorVersion, "operator_version", operatorVersion.String()) + return tempo, nil + } + + for _, availableUpgrade := range upgrades { + if availableUpgrade.version.GreaterThan(instanceVersion) { + upgraded, err := availableUpgrade.upgrade(u, &tempo) + if err != nil { + log.Error(err, "failed to upgrade TempoStack instance", "from_version", tempo.Status.OperatorVersion, "to_version", availableUpgrade.version.String()) + return tempo, err + } + + log.V(1).Info("performed upgrade step", "from_version", tempo.Status.OperatorVersion, "to_version", availableUpgrade.version.String()) + upgraded.Status.OperatorVersion = availableUpgrade.version.String() + tempo = *upgraded + } + } + + // update all tempo images to the new default images on every upgrade + updateTempoStackImages(u, &tempo) + // at the end of the upgrade process, the CR is up to date with the current running operator version + tempo.Status.OperatorVersion = u.Version.OperatorVersion + + return tempo, nil +} + +// updateTempoStackImages updates all images with the default images of the operator configuration +func updateTempoStackImages(u Upgrade, tempo *v1alpha1.TempoStack) { + tempo.Spec.Images.Tempo = u.CtrlConfig.DefaultImages.Tempo + tempo.Spec.Images.TempoQuery = u.CtrlConfig.DefaultImages.TempoQuery + tempo.Spec.Images.TempoGateway = u.CtrlConfig.DefaultImages.TempoGateway + tempo.Spec.Images.TempoGatewayOpa = u.CtrlConfig.DefaultImages.TempoGatewayOpa +} diff --git a/internal/upgrade/upgrade_test.go b/internal/upgrade/upgrade_test.go new file mode 100644 index 000000000..c69d0f61f --- /dev/null +++ b/internal/upgrade/upgrade_test.go @@ -0,0 +1,146 @@ +package upgrade + +import ( + "context" + "testing" + + configv1alpha1 "github.com/os-observability/tempo-operator/apis/config/v1alpha1" + "github.com/os-observability/tempo-operator/apis/tempo/v1alpha1" + "github.com/os-observability/tempo-operator/internal/version" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var logger = log.Log.WithName("unit-tests") + +func createTempoCR(t *testing.T, nsn types.NamespacedName, version string) *v1alpha1.TempoStack { + tempo := &v1alpha1.TempoStack{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsn.Name, + Namespace: nsn.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "tempo-operator", + }, + }, + Spec: v1alpha1.TempoStackSpec{ + Images: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:0.0.0", + TempoQuery: "docker.io/grafana/tempo-query:0.0.0", + TempoGateway: "quay.io/observatorium/api:0.0.0", + TempoGatewayOpa: "quay.io/observatorium/opa-openshift:0.0.0", + }, + Storage: v1alpha1.ObjectStorageSpec{ + Secret: v1alpha1.ObjectStorageSecretSpec{ + Type: "s3", + }, + }, + }, + } + + err := k8sClient.Create(context.Background(), tempo) + require.NoError(t, err) + + tempo.Status.OperatorVersion = version + err = k8sClient.Status().Update(context.Background(), tempo) + require.NoError(t, err) + + return tempo +} + +func TestUpgradeToLatest(t *testing.T) { + latestVersion := upgrades[len(upgrades)-1].version.String() + + nsn := types.NamespacedName{Name: "upgrade-to-latest-test", Namespace: "default"} + createTempoCR(t, nsn, "0.0.1") + + currentV := version.Get() + currentV.OperatorVersion = "0.1.0" + + upgrade := &Upgrade{ + Client: k8sClient, + Recorder: record.NewFakeRecorder(1), + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:latest", + TempoQuery: "docker.io/grafana/tempo-query:latest", + TempoGateway: "quay.io/observatorium/api:latest", + TempoGatewayOpa: "quay.io/observatorium/opa-openshift:latest", + }, + }, + Version: currentV, + Log: logger, + } + err := upgrade.TempoStacks(context.Background()) + require.NoError(t, err) + + upgradedTempo := v1alpha1.TempoStack{} + err = k8sClient.Get(context.Background(), nsn, &upgradedTempo) + assert.NoError(t, err) + assert.Equal(t, latestVersion, upgradedTempo.Status.OperatorVersion) + + // assert images were updated + assert.Equal(t, "docker.io/grafana/tempo:latest", upgradedTempo.Spec.Images.Tempo) + assert.Equal(t, "docker.io/grafana/tempo-query:latest", upgradedTempo.Spec.Images.TempoQuery) + assert.Equal(t, "quay.io/observatorium/api:latest", upgradedTempo.Spec.Images.TempoGateway) + assert.Equal(t, "quay.io/observatorium/opa-openshift:latest", upgradedTempo.Spec.Images.TempoGatewayOpa) +} + +func TestSkipUpgrade(t *testing.T) { + tests := []struct { + name string + startVersion string + upgradedVersion string + }{ + // Skip upgrade if the in-cluster version of the CR is more recent than the operator version + // For example, in case an old operator version got deployed by mistake + {"newer-than-ours", "10.0.0", "10.0.0"}, + + // Do not perform upgrade and do not update any images + {"up-to-date", "5.0.0", "5.0.0"}, + + // Ignore unparseable versions + {"unparseable", "abc", "abc"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nsn := types.NamespacedName{Name: "upgrade-test-" + test.name, Namespace: "default"} + originalTempo := createTempoCR(t, nsn, test.startVersion) + + currentV := version.Get() + currentV.OperatorVersion = "5.0.0" + + upgrade := &Upgrade{ + Client: k8sClient, + Recorder: record.NewFakeRecorder(1), + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:latest", + TempoQuery: "docker.io/grafana/tempo-query:latest", + TempoGateway: "quay.io/observatorium/api:latest", + TempoGatewayOpa: "quay.io/observatorium/opa-openshift:latest", + }, + }, + Version: currentV, + Log: logger, + } + err := upgrade.TempoStacks(context.Background()) + require.NoError(t, err) + + upgradedTempo := v1alpha1.TempoStack{} + err = k8sClient.Get(context.Background(), nsn, &upgradedTempo) + assert.NoError(t, err) + assert.Equal(t, test.upgradedVersion, upgradedTempo.Status.OperatorVersion) + + // assert images were not updated + assert.Equal(t, originalTempo.Spec.Images.Tempo, upgradedTempo.Spec.Images.Tempo) + assert.Equal(t, originalTempo.Spec.Images.TempoQuery, upgradedTempo.Spec.Images.TempoQuery) + assert.Equal(t, originalTempo.Spec.Images.TempoGateway, upgradedTempo.Spec.Images.TempoGateway) + assert.Equal(t, originalTempo.Spec.Images.TempoGatewayOpa, upgradedTempo.Spec.Images.TempoGatewayOpa) + }) + } +} diff --git a/internal/upgrade/v0_1_0.go b/internal/upgrade/v0_1_0.go new file mode 100644 index 000000000..8ea679029 --- /dev/null +++ b/internal/upgrade/v0_1_0.go @@ -0,0 +1,9 @@ +package upgrade + +import "github.com/os-observability/tempo-operator/apis/tempo/v1alpha1" + +// this is a template for future versions +func upgrade0_1_0(u Upgrade, tempo *v1alpha1.TempoStack) (*v1alpha1.TempoStack, error) { + // no-op because 0.1.0 is the first released tempo-operator version + return tempo, nil +} diff --git a/internal/upgrade/versions.go b/internal/upgrade/versions.go new file mode 100644 index 000000000..52f45c1a9 --- /dev/null +++ b/internal/upgrade/versions.go @@ -0,0 +1,24 @@ +package upgrade + +import ( + "github.com/Masterminds/semver/v3" + "github.com/os-observability/tempo-operator/apis/tempo/v1alpha1" +) + +type upgradeFunc func(u Upgrade, tempo *v1alpha1.TempoStack) (*v1alpha1.TempoStack, error) + +type versionUpgrade struct { + version semver.Version + upgrade upgradeFunc +} + +var ( + // List of all operator versions requiring "manual" upgrade steps + // This list needs to be sorted by the version ascending + upgrades = []versionUpgrade{ + { + version: *semver.MustParse("0.1.0"), + upgrade: upgrade0_1_0, + }, + } +)