From 0255957dd70b8dc1aa8882b12c88ca0b4b81b3aa Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Oct 2025 15:08:36 +0300 Subject: [PATCH] Improve `flux migrate` for live cluster migrations Signed-off-by: Stefan Prodan --- cmd/flux/migrate.go | 93 +++++++++++++++++++++++++++------------------ go.mod | 2 +- go.sum | 4 +- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/cmd/flux/migrate.go b/cmd/flux/migrate.go index 904196cd..3a0129ce 100644 --- a/cmd/flux/migrate.go +++ b/cmd/flux/migrate.go @@ -18,18 +18,21 @@ package main import ( "context" + "encoding/json" "fmt" "io/fs" "os" "path/filepath" "strings" + "github.com/fluxcd/pkg/ssa" "github.com/manifoldco/promptui" "github.com/spf13/cobra" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" @@ -54,6 +57,7 @@ type APIVersions struct { LatestVersions map[schema.GroupKind]string } +// TODO: Update this mapping when new Flux minor versions are released! // latestAPIVersions contains the latest API versions for each GroupKind // for each supported Flux version. We maintain the latest two minor versions. var latestAPIVersions = []APIVersions{ @@ -132,35 +136,29 @@ The command has two modes of operation: - Cluster mode (default): migrates all the Flux custom resources stored in Kubernetes etcd to their latest API version. - File system mode (-f): migrates the Flux custom resources defined in the manifests located in the specified path. - -Examples: - - # Migrate all the Flux custom resources in the cluster. +`, + Example: ` # Migrate all the Flux custom resources in the cluster. + # This uses the current kubeconfig context and requires cluster-admin permissions. flux migrate - # Migrate all manifests in a Git repository. + # Migrate all the Flux custom resources in a Git repository + # checked out in the current working directory. flux migrate -f . - # Migrate the Flux custom resources defined in the manifests located in the ./manifest.yaml file. - flux migrate --path=./manifest.yaml - - # Migrate the Flux custom resources defined in the manifests located in the ./manifests directory. - flux migrate --path=./manifests + # Migrate all Flux custom resources defined in YAML and Helm YAML template files. + flux migrate -f . --extensions=.yml,.yaml,.tpl - # Migrate the Flux custom resources defined in the manifests located in the ./manifests directory - # skipping confirmation prompts. - flux migrate --path=./manifests --yes + # Migrate the Flux custom resources to the latest API versions of Flux 2.6. + flux migrate -f . --version=2.6 - # Simulate the migration process without making any changes, only applicable with --path. - flux migrate --path=./manifests --dry-run + # Migrate the Flux custom resources defined in a multi-document YAML manifest file. + flux migrate -f path/to/manifest.yaml - # Migrate the Flux custom resources defined in the manifests located in the ./manifests directory - # considering YAML and Helm YAML template files. - flux migrate --path=./manifests --extensions=.yml --extensions=.yaml --extensions=.tpl + # Simulate the migration without making any changes. + flux migrate -f . --dry-run - # Migrate the Flux custom resources defined in the manifests located in the ./manifests directory - # for Flux 2.6. This will migrate the custom resources to their latest API versions in Flux 2.6. - flux migrate --path=./manifests --version=2.6 + # Run the migration by skipping confirmation prompts. + flux migrate -f . --yes `, RunE: runMigrateCmd, } @@ -176,16 +174,16 @@ var migrateFlags struct { func init() { rootCmd.AddCommand(migrateCmd) - migrateCmd.Flags().BoolVarP(&migrateFlags.yes, "yes", "y", false, - "skip confirmation prompts") - migrateCmd.Flags().BoolVar(&migrateFlags.dryRun, "dry-run", false, - "simulate the migration process without making any changes, only applicable with --path") migrateCmd.Flags().StringVarP(&migrateFlags.path, "path", "f", "", "the path to the directory containing the manifests to migrate") - migrateCmd.Flags().StringArrayVarP(&migrateFlags.extensions, "extensions", "e", []string{".yaml", ".yml"}, - "the file extensions to consider when migrating manifests from the filesystem (only used with --path)") + migrateCmd.Flags().StringSliceVarP(&migrateFlags.extensions, "extensions", "e", []string{".yaml", ".yml"}, + "the file extensions to consider when migrating manifests, only applicable --path") migrateCmd.Flags().StringVarP(&migrateFlags.version, "version", "v", "", - "the target Flux version to migrate custom resource API versions to (defaults to the version of the CLI)") + "the target Flux minor version to migrate manifests to, only applicable with --path (defaults to the version of the CLI)") + migrateCmd.Flags().BoolVarP(&migrateFlags.yes, "yes", "y", false, + "skip confirmation prompts when migrating manifests, only applicable with --path") + migrateCmd.Flags().BoolVar(&migrateFlags.dryRun, "dry-run", false, + "simulate the migration of manifests without making any changes, only applicable with --path") } func runMigrateCmd(*cobra.Command, []string) error { @@ -319,7 +317,7 @@ func (c *ClusterMigrator) migrateCRD(ctx context.Context, name string) error { return nil } -// migrateCR migrates all CRs for the given CRD to the specified version by patching them with an empty patch. +// migrateCR migrates all CRs for the given CRD to the specified version by patching them. func (c *ClusterMigrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, version string) error { list := &unstructured.UnstructuredList{} @@ -339,16 +337,39 @@ func (c *ClusterMigrator) migrateCR(ctx context.Context, crd *apiextensionsv1.Cu } for _, item := range list.Items { - // patch the resource with an empty patch to update the version - if err := c.kubeClient.Patch( - ctx, - &item, - client.RawPatch(client.Merge.Type(), []byte("{}")), - ); err != nil && !apierrors.IsNotFound(err) { - return fmt.Errorf(" %s/%s/%s failed to migrate: %w", + patches, err := ssa.PatchMigrateToVersion(&item, apiVersion) + if err != nil { + return fmt.Errorf("failed to create migration patch for %s/%s/%s: %w", item.GetKind(), item.GetNamespace(), item.GetName(), err) } + if len(patches) == 0 { + // patch the resource with an empty patch to update the version + if err := c.kubeClient.Patch( + ctx, + &item, + client.RawPatch(client.Merge.Type(), []byte("{}")), + ); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf(" %s/%s/%s failed to migrate: %w", + item.GetKind(), item.GetNamespace(), item.GetName(), err) + } + } else { + // patch the resource to migrate the managed fields to the latest apiVersion + rawPatch, err := json.Marshal(patches) + if err != nil { + return fmt.Errorf("failed to marshal migration patch for %s/%s/%s: %w", + item.GetKind(), item.GetNamespace(), item.GetName(), err) + } + if err := c.kubeClient.Patch( + ctx, + &item, + client.RawPatch(types.JSONPatchType, rawPatch), + ); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf(" %s/%s/%s failed to migrate managed fields: %w", + item.GetKind(), item.GetNamespace(), item.GetName(), err) + } + } + logger.Successf("%s/%s/%s migrated to version %s", item.GetKind(), item.GetNamespace(), item.GetName(), version) } diff --git a/go.mod b/go.mod index fa4197af..77f26734 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/fluxcd/pkg/oci v0.56.0 github.com/fluxcd/pkg/runtime v0.86.0 github.com/fluxcd/pkg/sourceignore v0.14.0 - github.com/fluxcd/pkg/ssa v0.58.0 + github.com/fluxcd/pkg/ssa v0.59.0 github.com/fluxcd/pkg/ssh v0.21.0 github.com/fluxcd/pkg/tar v0.14.0 github.com/fluxcd/pkg/version v0.10.0 diff --git a/go.sum b/go.sum index 7e85bb3a..b6f56984 100644 --- a/go.sum +++ b/go.sum @@ -214,8 +214,8 @@ github.com/fluxcd/pkg/runtime v0.86.0 h1:q7aBSerJwt0N9hpurPVElG+HWpVhZcs6t96bcNQ github.com/fluxcd/pkg/runtime v0.86.0/go.mod h1:Wt9mUzQgMPQMu2D/wKl5pG4zh5vu/tfF5wq9pPobxOQ= github.com/fluxcd/pkg/sourceignore v0.14.0 h1:ZiZzbXtXb/Qp7I7JCStsxOlX8ri8rWwCvmvIrJ0UzQQ= github.com/fluxcd/pkg/sourceignore v0.14.0/go.mod h1:E3zKvyTyB+oQKqm/2I/jS6Rrt3B7fNuig/4bY2vi3bg= -github.com/fluxcd/pkg/ssa v0.58.0 h1:W7m2LQFsZxPN9nn3lfGVDwXsZnIgCWWJ/+/K5hpzW+k= -github.com/fluxcd/pkg/ssa v0.58.0/go.mod h1:iN/QDMqdJaVXKkqwbXqGa4PyWQwtyIy2WkeM2+9kfXA= +github.com/fluxcd/pkg/ssa v0.59.0 h1:c88Q5w9e0MgrEi3Z7/+FWEVvtJFaVHfA9sxreMJUR7g= +github.com/fluxcd/pkg/ssa v0.59.0/go.mod h1:iN/QDMqdJaVXKkqwbXqGa4PyWQwtyIy2WkeM2+9kfXA= github.com/fluxcd/pkg/ssh v0.21.0 h1:ZmyF0n9je0cTTkOpvFVgIhmdx9qtswnVE60TK4IzJh0= github.com/fluxcd/pkg/ssh v0.21.0/go.mod h1:nX+gvJOmjf0E7lxq5mKKzDIdPEL2jOUQZbkBMS+mDtk= github.com/fluxcd/pkg/tar v0.14.0 h1:9Gku8FIvPt2bixKldZnzXJ/t+7SloxePlzyVGOK8GVQ=