diff --git a/cmd/ctl/pkg/upgrade/migrate/BUILD.bazel b/cmd/ctl/pkg/upgrade/migrate/BUILD.bazel index 98e21a178..287efe6e6 100644 --- a/cmd/ctl/pkg/upgrade/migrate/BUILD.bazel +++ b/cmd/ctl/pkg/upgrade/migrate/BUILD.bazel @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["migrate.go"], + srcs = [ + "command.go", + "migrator.go", + ], importpath = "github.com/jetstack/cert-manager/cmd/ctl/pkg/upgrade/migrate", visibility = ["//visibility:public"], deps = [ diff --git a/cmd/ctl/pkg/upgrade/migrate/command.go b/cmd/ctl/pkg/upgrade/migrate/command.go new file mode 100644 index 000000000..e5a7f5078 --- /dev/null +++ b/cmd/ctl/pkg/upgrade/migrate/command.go @@ -0,0 +1,123 @@ +/* +Copyright 2022 The cert-manager Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package migrate + +import ( + "context" + + "github.com/spf13/cobra" + apiextinstall "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/jetstack/cert-manager/cmd/ctl/pkg/build" + "github.com/jetstack/cert-manager/cmd/ctl/pkg/factory" + acmeinstall "github.com/jetstack/cert-manager/internal/apis/acme/install" + cminstall "github.com/jetstack/cert-manager/internal/apis/certmanager/install" +) + +var ( + long = templates.LongDesc(i18n.T(` +Ensures resources in your Kubernetes cluster are persisted in the v1 API version. + +This must be run prior to upgrading to ensure your cluster is ready to upgrade to cert-manager v1.7 and beyond. + +This command must be run with a cluster running cert-manager v1.0 or greater.`)) + + example = templates.Examples(i18n.T(build.WithTemplate(` +# Check the cert-manager installation is ready to be upgraded to v1.7 +{{.BuildName}} upgrade migrate +`))) +) + +// Options is a struct to support renew command +type Options struct { + genericclioptions.IOStreams + *factory.Factory + + client client.Client + force bool +} + +// NewOptions returns initialized Options +func NewOptions(ioStreams genericclioptions.IOStreams) *Options { + return &Options{ + IOStreams: ioStreams, + } +} + +// NewCmdMigrate returns a cobra command for updating resources in an apiserver +// to force a new storage version to be used. +func NewCmdMigrate(ctx context.Context, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + cmd := &cobra.Command{ + Use: "migrate", + Short: "Migrate all existing persisted cert-manager resources to the v1 API version", + Long: long, + Example: example, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Validate(args)) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run(ctx, args)) + }, + } + + cmd.Flags().BoolVarP(&o.force, "force", "f", o.force, ""+ + "If true, all resources will be read and written regardless of the 'status.storedVersions' on the CRD resource. "+ + "Use this mode if you have previously manually modified the 'status.storedVersions' field on CRD resources.") + + o.Factory = factory.New(ctx, cmd) + + return cmd +} + +// Validate validates the provided options +func (o *Options) Validate(_ []string) error { + return nil +} + +// Complete takes the command arguments and factory and infers any remaining options. +func (o *Options) Complete() error { + var err error + scheme := runtime.NewScheme() + apiextinstall.Install(scheme) + cminstall.Install(scheme) + acmeinstall.Install(scheme) + + o.client, err = client.New(o.RESTConfig, client.Options{Scheme: scheme}) + if err != nil { + return err + } + + return nil +} + +// Run executes renew command +func (o *Options) Run(ctx context.Context, args []string) error { + return NewMigrator(o.client, o.force, o.Out, o.ErrOut).Run(ctx, "v1", []string{ + "certificates.cert-manager.io", + "certificaterequests.cert-manager.io", + "issuers.cert-manager.io", + "clusterissuers.cert-manager.io", + "orders.acme.cert-manager.io", + "challenges.acme.cert-manager.io", + }) +} diff --git a/cmd/ctl/pkg/upgrade/migrate/migrate.go b/cmd/ctl/pkg/upgrade/migrate/migrate.go deleted file mode 100644 index e73ab28fa..000000000 --- a/cmd/ctl/pkg/upgrade/migrate/migrate.go +++ /dev/null @@ -1,282 +0,0 @@ -/* -Copyright 2022 The cert-manager Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package migrate - -import ( - "context" - "fmt" - "time" - - "github.com/spf13/cobra" - apiextinstall "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" - apiext "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" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/i18n" - "k8s.io/kubectl/pkg/util/templates" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/jetstack/cert-manager/cmd/ctl/pkg/build" - "github.com/jetstack/cert-manager/cmd/ctl/pkg/factory" - acmeinstall "github.com/jetstack/cert-manager/internal/apis/acme/install" - cminstall "github.com/jetstack/cert-manager/internal/apis/certmanager/install" -) - -var ( - long = templates.LongDesc(i18n.T(` -Ensures resources in your Kubernetes cluster are persisted in the v1 API version. - -This must be run prior to upgrading to ensure your cluster is ready to upgrade to cert-manager v1.7 and beyond. - -This command must be run with a cluster running cert-manager v1.0 or greater.`)) - - example = templates.Examples(i18n.T(build.WithTemplate(` -# Check the cert-manager installation is ready to be upgraded to v1.7 -{{.BuildName}} upgrade migrate -`))) -) - -var scheme = runtime.NewScheme() - -func init() { - apiextinstall.Install(scheme) - cminstall.Install(scheme) - acmeinstall.Install(scheme) -} - -// Options is a struct to support renew command -type Options struct { - genericclioptions.IOStreams - *factory.Factory - - client client.Client -} - -// NewOptions returns initialized Options -func NewOptions(ioStreams genericclioptions.IOStreams) *Options { - return &Options{ - IOStreams: ioStreams, - } -} - -// NewCmdMigrate returns a cobra command for updating resources in an apiserver -// to force a new storage version to be used. -func NewCmdMigrate(ctx context.Context, ioStreams genericclioptions.IOStreams) *cobra.Command { - o := NewOptions(ioStreams) - cmd := &cobra.Command{ - Use: "migrate", - Short: "Migrate all existing persisted cert-manager resources to the v1 API version", - Long: long, - Example: example, - Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(o.Validate(cmd, args)) - cmdutil.CheckErr(o.Complete()) - cmdutil.CheckErr(o.Run(ctx, args)) - }, - } - - o.Factory = factory.New(ctx, cmd) - - return cmd -} - -// Validate validates the provided options -func (o *Options) Validate(cmd *cobra.Command, args []string) error { - return nil -} - -// Complete takes the command arguments and factory and infers any remaining options. -func (o *Options) Complete() error { - var err error - o.client, err = client.New(o.RESTConfig, client.Options{Scheme: scheme}) - if err != nil { - return err - } - - return nil -} - -// Run executes renew command -func (o *Options) Run(ctx context.Context, args []string) error { - // Check all cert-manager CRDs to ensure that they have their - // storage version set to v1. - allCRDNames := []string{ - "certificates.cert-manager.io", - "certificaterequests.cert-manager.io", - "issuers.cert-manager.io", - "clusterissuers.cert-manager.io", - "orders.acme.cert-manager.io", - "challenges.acme.cert-manager.io", - } - - fmt.Fprintln(o.Out, "Checking all cert-manager CustomResourceDefinitions have storage version set to 'v1'") - allV1, crdsRequiringMigration, err := o.ensureCRDStorageVersionEquals(ctx, "v1", allCRDNames) - if err != nil { - return err - } - if !allV1 { - fmt.Fprintln(o.ErrOut, "It looks like you are running a pre-1.0 version of cert-manager. Please upgrade cert-manager to v1.6 before upgrading to v1.7.") - return fmt.Errorf("migration failed") - } - fmt.Fprintln(o.Out, "All CustomResourceDefinitions have 'v1' configured as the storage version.") - - /* - fmt.Fprintln(o.Out, "Looking for CRDs that contain resources that require migrating to 'v1'...") - crdsRequiringMigration, err := o.discoverCRDsRequiringMigration(ctx, "v1", allCRDNames) - if err != nil { - fmt.Fprintf(o.ErrOut, "Failed to determine resource types that require migration: %v\n", err) - return err - } - if len(crdsRequiringMigration) == 0 { - fmt.Fprintln(o.Out, "Nothing to do. cert-manager CRDs do not have 'status.storedVersions' containing old API versions. You may proceed to upgrade to cert-manager v1.7.") - return nil - } - */ - - fmt.Fprintf(o.Out, "Found %d resource types that require migration:\n", len(crdsRequiringMigration)) - for _, crd := range crdsRequiringMigration { - fmt.Fprintf(o.Out, " - %s\n", crd.Name) - } - - for _, crd := range crdsRequiringMigration { - if err := o.migrateResourcesForCRD(ctx, crd); err != nil { - fmt.Fprintf(o.ErrOut, "Failed to migrate resource: %v\n", err) - return err - } - } - - fmt.Fprintln(o.Out, "Patching CRD resources to set 'status.storedVersions' to 'v1'...") - if err := o.patchCRDStoredVersions(ctx, crdsRequiringMigration); err != nil { - fmt.Fprintf(o.ErrOut, "Failed to patch 'status.storedVersions' field: %v\n", err) - return err - } - - fmt.Fprintln(o.Out, "Successfully migrated all cert-manager resource types. It is now safe to proceed with upgrading to cert-manager v1.7.") - return nil -} - -func (o *Options) ensureCRDStorageVersionEquals(ctx context.Context, vers string, names []string) (bool, []*apiext.CustomResourceDefinition, error) { - var crds []*apiext.CustomResourceDefinition - for _, crdName := range names { - crd := &apiext.CustomResourceDefinition{} - if err := o.client.Get(ctx, client.ObjectKey{Name: crdName}, crd); err != nil { - return false, nil, err - } - - // Discover the storage version - storageVersion := "" - for _, v := range crd.Spec.Versions { - if v.Storage { - storageVersion = v.Name - break - } - } - - if storageVersion != vers { - fmt.Fprintf(o.Out, "CustomResourceDefinition object %q has storage version set to %q. You MUST upgrade to cert-manager v1.0-v1.6 before migrating resources for v1.7.\n", crdName, storageVersion) - return false, nil, nil - } - - crds = append(crds, crd) - } - - return true, crds, nil -} - -func (o *Options) discoverCRDsRequiringMigration(ctx context.Context, desiredStorageVersion string, names []string) ([]*apiext.CustomResourceDefinition, error) { - var requireMigration []*apiext.CustomResourceDefinition - for _, name := range names { - crd := &apiext.CustomResourceDefinition{} - if err := o.client.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil { - return nil, err - } - // If no versions are stored, there's nothing to migrate. - if len(crd.Status.StoredVersions) == 0 { - continue - } - // If more than one entry exists in `storedVersions` OR if the only element in there is not - // the desired version, perform a migration. - if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != desiredStorageVersion { - requireMigration = append(requireMigration, crd) - } - } - return requireMigration, nil -} - -func (o *Options) migrateResourcesForCRD(ctx context.Context, crd *apiext.CustomResourceDefinition) error { - startTime := time.Now() - fmt.Fprintf(o.Out, "Migrating %q objects in group %q - this may take a while (started at %s)...\n", crd.Spec.Names.Kind, crd.Spec.Group, startTime.Format(time.Stamp)) - list := &unstructured.UnstructuredList{} - list.SetGroupVersionKind(schema.GroupVersionKind{ - Group: crd.Spec.Group, - Version: "v1", - Kind: crd.Spec.Names.ListKind, - }) - if err := o.client.List(ctx, list); err != nil { - return err - } - fmt.Fprintf(o.Out, " %d resources to migrate\n", len(list.Items)) - for _, obj := range list.Items { - if err := o.client.Update(ctx, &obj); handleUpdateErr(err) != nil { - return err - } - } - fmt.Fprintf(o.Out, " Successfully migrated %d %s objects in %s\n", len(list.Items), crd.Spec.Names.Kind, time.Now().Sub(startTime).Round(time.Second)) - return nil -} - -// handleUpdateErr will absorb certain types of errors that we know can be skipped/passed on -// during a migration of a particular object. -func handleUpdateErr(err error) error { - if err == nil { - return nil - } - // If the resource no longer exists, don't return the error as the object no longer - // needs updating to the new API version. - if apierrors.IsNotFound(err) { - return nil - } - // If there was a conflict, another client must have written the object already which - // means we don't need to force an update. - if apierrors.IsConflict(err) { - return nil - } - return err -} - -func (o *Options) patchCRDStoredVersions(ctx context.Context, crds []*apiext.CustomResourceDefinition) error { - for _, crd := range crds { - // fetch a fresh copy of the CRD to avoid any conflict errors - freshCRD := &apiext.CustomResourceDefinition{} - if err := o.client.Get(ctx, client.ObjectKey{Name: crd.Name}, freshCRD); err != nil { - return err - } - - // Set the `status.storedVersions` field to 'v1' - freshCRD.Status.StoredVersions = []string{"v1"} - - if err := o.client.Status().Update(ctx, freshCRD); err != nil { - return err - } - } - - return nil -} diff --git a/cmd/ctl/pkg/upgrade/migrate/migrator.go b/cmd/ctl/pkg/upgrade/migrate/migrator.go new file mode 100644 index 000000000..7d05427c7 --- /dev/null +++ b/cmd/ctl/pkg/upgrade/migrate/migrator.go @@ -0,0 +1,207 @@ +package migrate + +import ( + "context" + "fmt" + "io" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "time" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Migrator struct { + // Client used for API interactions + Client client.Client + + // If true, skip checking the 'status.storedVersion' before running the migration. + // By default, migration will only be run if the CRD contains storedVersions other + // than the desired target version. + Force bool + + // Writers to write informational & error messages to + Out, ErrOut io.Writer +} + +// NewMigrator creates a new migrator with the given API client. +// If either of out or errOut are nil, log messages will be discarded. +func NewMigrator(client client.Client, force bool, out, errOut io.Writer) *Migrator { + if out == nil { + out = io.Discard + } + if errOut == nil { + errOut = io.Discard + } + + return &Migrator{ + Client: client, + Force: force, + Out: out, + ErrOut: errOut, + } +} + +// Run begins the migration of all the named CRDs. +// It will attempt to migrate all resources defined as part of these CRDs to the +// given 'targetVersion', and after completion will update the `status.storedVersions` +// field on the corresponding CRD version to only contain the given targetVersion. +func (m *Migrator) Run(ctx context.Context, targetVersion string, names []string) error { + fmt.Fprintf(m.Out, "Checking all CustomResourceDefinitions have storage version set to '%s'\n", targetVersion) + allV1, allCRDs, err := m.ensureCRDStorageVersionEquals(ctx, targetVersion, names) + if err != nil { + return err + } + if !allV1 { + fmt.Fprintln(m.ErrOut, "It looks like you are running a pre-1.0 version of cert-manager. Please upgrade cert-manager to v1.6 before upgrading to v1.7.") + return fmt.Errorf("preflight checks failed") + } + fmt.Fprintf(m.Out, "All CustomResourceDefinitions have %q configured as the storage version.\n", targetVersion) + + crdsRequiringMigration := allCRDs + if !m.Force { + fmt.Fprintln(m.Out, "Looking for CRDs that contain resources that require migrating to 'v1'...") + crdsRequiringMigration, err = m.discoverCRDsRequiringMigration(ctx, "v1", names) + if err != nil { + fmt.Fprintf(m.ErrOut, "Failed to determine resource types that require migration: %v\n", err) + return err + } + if len(crdsRequiringMigration) == 0 { + fmt.Fprintln(m.Out, "Nothing to do. cert-manager CRDs do not have 'status.storedVersions' containing old API versions. You may proceed to upgrade to cert-manager v1.7.") + return nil + } + } else { + fmt.Fprintln(m.Out, "Forcing migration of all CRD resources as --force=true") + } + + fmt.Fprintf(m.Out, "Found %d resource types that require migration:\n", len(crdsRequiringMigration)) + for _, crd := range crdsRequiringMigration { + fmt.Fprintf(m.Out, " - %s\n", crd.Name) + } + + for _, crd := range crdsRequiringMigration { + if err := m.migrateResourcesForCRD(ctx, crd); err != nil { + fmt.Fprintf(m.ErrOut, "Failed to migrate resource: %v\n", err) + return err + } + } + + fmt.Fprintf(m.Out, "Patching CRD resources to set 'status.storedVersions' to %q...\n", targetVersion) + if err := m.patchCRDStoredVersions(ctx, crdsRequiringMigration); err != nil { + fmt.Fprintf(m.ErrOut, "Failed to patch 'status.storedVersions' field: %v\n", err) + return err + } + + fmt.Fprintln(m.Out, "Successfully migrated all cert-manager resource types. It is now safe to proceed with upgrading to cert-manager v1.7.") + return nil +} + +func (m *Migrator) ensureCRDStorageVersionEquals(ctx context.Context, vers string, names []string) (bool, []*apiext.CustomResourceDefinition, error) { + var crds []*apiext.CustomResourceDefinition + for _, crdName := range names { + crd := &apiext.CustomResourceDefinition{} + if err := m.Client.Get(ctx, client.ObjectKey{Name: crdName}, crd); err != nil { + return false, nil, err + } + + // Discover the storage version + storageVersion := "" + for _, v := range crd.Spec.Versions { + if v.Storage { + storageVersion = v.Name + break + } + } + + if storageVersion != vers { + fmt.Fprintf(m.Out, "CustomResourceDefinition object %q has storage version set to %q. You MUST upgrade to cert-manager v1.0-v1.6 before migrating resources for v1.7.\n", crdName, storageVersion) + return false, nil, nil + } + + crds = append(crds, crd) + } + + return true, crds, nil +} + +func (m *Migrator) discoverCRDsRequiringMigration(ctx context.Context, desiredStorageVersion string, names []string) ([]*apiext.CustomResourceDefinition, error) { + var requireMigration []*apiext.CustomResourceDefinition + for _, name := range names { + crd := &apiext.CustomResourceDefinition{} + if err := m.Client.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil { + return nil, err + } + // If no versions are stored, there's nothing to migrate. + if len(crd.Status.StoredVersions) == 0 { + continue + } + // If more than one entry exists in `storedVersions` OR if the only element in there is not + // the desired version, perform a migration. + if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != desiredStorageVersion { + requireMigration = append(requireMigration, crd) + } + } + return requireMigration, nil +} + +func (m *Migrator) migrateResourcesForCRD(ctx context.Context, crd *apiext.CustomResourceDefinition) error { + startTime := time.Now() + fmt.Fprintf(m.Out, "Migrating %q objects in group %q - this may take a while (started at %s)...\n", crd.Spec.Names.Kind, crd.Spec.Group, startTime.Format(time.Stamp)) + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: "v1", + Kind: crd.Spec.Names.ListKind, + }) + if err := m.Client.List(ctx, list); err != nil { + return err + } + fmt.Fprintf(m.Out, " %d resources to migrate\n", len(list.Items)) + for _, obj := range list.Items { + if err := m.Client.Update(ctx, &obj); handleUpdateErr(err) != nil { + return err + } + } + fmt.Fprintf(m.Out, " Successfully migrated %d %s objects in %s\n", len(list.Items), crd.Spec.Names.Kind, time.Now().Sub(startTime).Round(time.Second)) + return nil +} + +func (m *Migrator) patchCRDStoredVersions(ctx context.Context, crds []*apiext.CustomResourceDefinition) error { + for _, crd := range crds { + // fetch a fresh copy of the CRD to avoid any conflict errors + freshCRD := &apiext.CustomResourceDefinition{} + if err := m.Client.Get(ctx, client.ObjectKey{Name: crd.Name}, freshCRD); err != nil { + return err + } + + // Set the `status.storedVersions` field to 'v1' + freshCRD.Status.StoredVersions = []string{"v1"} + + if err := m.Client.Status().Update(ctx, freshCRD); err != nil { + return err + } + } + + return nil +} + +// handleUpdateErr will absorb certain types of errors that we know can be skipped/passed on +// during a migration of a particular object. +func handleUpdateErr(err error) error { + if err == nil { + return nil + } + // If the resource no longer exists, don't return the error as the object no longer + // needs updating to the new API version. + if apierrors.IsNotFound(err) { + return nil + } + // If there was a conflict, another client must have written the object already which + // means we don't need to force an update. + if apierrors.IsConflict(err) { + return nil + } + return err +}