diff --git a/cmd/cli/kubectl-kyverno/commands/command.go b/cmd/cli/kubectl-kyverno/commands/command.go index 1ce5947a6d..f1a64efda0 100644 --- a/cmd/cli/kubectl-kyverno/commands/command.go +++ b/cmd/cli/kubectl-kyverno/commands/command.go @@ -7,6 +7,7 @@ import ( "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/docs" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/fix" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/jp" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/migrate" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/oci" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/test" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/version" @@ -28,6 +29,7 @@ func RootCommand(experimental bool) *cobra.Command { create.Command(), docs.Command(cmd), jp.Command(), + migrate.Command(), test.Command(), version.Command(), ) diff --git a/cmd/cli/kubectl-kyverno/commands/command_test.go b/cmd/cli/kubectl-kyverno/commands/command_test.go index a5bb71e116..be06b2f835 100644 --- a/cmd/cli/kubectl-kyverno/commands/command_test.go +++ b/cmd/cli/kubectl-kyverno/commands/command_test.go @@ -12,7 +12,7 @@ import ( func TestRootCommand(t *testing.T) { cmd := RootCommand(false) assert.NotNil(t, cmd) - assert.Len(t, cmd.Commands(), 6) + assert.Len(t, cmd.Commands(), 7) err := cmd.Execute() assert.NoError(t, err) } @@ -20,7 +20,7 @@ func TestRootCommand(t *testing.T) { func TestRootCommandExperimental(t *testing.T) { cmd := RootCommand(true) assert.NotNil(t, cmd) - assert.Len(t, cmd.Commands(), 8) + assert.Len(t, cmd.Commands(), 9) err := cmd.Execute() assert.NoError(t, err) } diff --git a/cmd/cli/kubectl-kyverno/commands/migrate/command.go b/cmd/cli/kubectl-kyverno/commands/migrate/command.go new file mode 100644 index 0000000000..21d5f3528f --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/migrate/command.go @@ -0,0 +1,119 @@ +package migrate + +import ( + "context" + "errors" + "fmt" + + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" + "github.com/kyverno/kyverno/pkg/config" + "github.com/spf13/cobra" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +type options struct { + KubeConfig string + Context string + Resources []string +} + +func Command() *cobra.Command { + var options options + cmd := &cobra.Command{ + Use: "migrate", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { + clientConfig, err := config.CreateClientConfigWithContext(options.KubeConfig, options.Context) + if err != nil { + return err + } + apiServerClient, err := clientset.NewForConfig(clientConfig) + if err != nil { + return err + } + dynamicClient, err := dynamic.NewForConfig(clientConfig) + if err != nil { + return err + } + for _, resource := range options.Resources { + fmt.Println("migrating resource:", resource, "...") + ctx := context.Background() + crd, err := apiServerClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, resource, metav1.GetOptions{}) + if err != nil { + return err + } + if err := migrate(ctx, crd, dynamicClient, apiServerClient); err != nil { + return err + } + } + return nil + }, + } + cmd.Flags().StringVar(&options.KubeConfig, "kubeconfig", "", "path to kubeconfig file with authorization and master location information") + cmd.Flags().StringVar(&options.Context, "context", "", "The name of the kubeconfig context to use") + cmd.Flags().StringSliceVar(&options.Resources, "resource", nil, "The resource to migrate") + return cmd +} + +func migrate(ctx context.Context, crd *v1.CustomResourceDefinition, dynamicClient dynamic.Interface, apiServerClient clientset.Interface) error { + var storedVersion *v1.CustomResourceDefinitionVersion + for i := range crd.Spec.Versions { + if crd.Spec.Versions[i].Storage { + storedVersion = &crd.Spec.Versions[i] + } + } + if storedVersion == nil { + return errors.New("stored version not found") + } else { + fmt.Println("stored version:", storedVersion.Name) + if len(crd.Status.StoredVersions) == 1 { + if crd.Status.StoredVersions[0] == storedVersion.Name { + fmt.Println("stored version is already up to date, nothing to do") + return nil + } + } + gvr := schema.GroupVersionResource{ + Group: crd.Spec.Group, + Version: storedVersion.Name, + Resource: crd.Spec.Names.Plural, + } + resource := dynamicClient.Resource(gvr) + var client dynamic.ResourceInterface + if crd.Spec.Scope == v1.NamespaceScoped { + client = resource.Namespace(metav1.NamespaceAll) + } else { + client = resource + } + fmt.Println("migrating resources...") + list, err := client.List(ctx, metav1.ListOptions{}) + if err != nil { + return err + } + for i := range list.Items { + var client dynamic.ResourceInterface + if crd.Spec.Scope == v1.NamespaceScoped { + client = resource.Namespace(list.Items[i].GetNamespace()) + } else { + client = resource + } + _, err := client.Update(ctx, &list.Items[i], metav1.UpdateOptions{}) + if err != nil { + return err + } + } + fmt.Println("patching status...") + crd.Status.StoredVersions = []string{storedVersion.Name} + if _, err := apiServerClient.ApiextensionsV1().CustomResourceDefinitions().UpdateStatus(ctx, crd, metav1.UpdateOptions{}); err != nil { + return err + } + } + return nil +} diff --git a/cmd/cli/kubectl-kyverno/commands/migrate/doc.go b/cmd/cli/kubectl-kyverno/commands/migrate/doc.go new file mode 100644 index 0000000000..7dd24e2d99 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/migrate/doc.go @@ -0,0 +1,15 @@ +package migrate + +// TODO +var websiteUrl = `` + +var description = []string{ + `Migrate one or more resources to the stored version.`, +} + +var examples = [][]string{ + { + `# Migrate policy exceptions`, + `kyverno migrate --resource policyexceptions.kyverno.io`, + }, +} diff --git a/docs/user/cli/commands/kyverno.md b/docs/user/cli/commands/kyverno.md index 5ea76f1ac9..892af29678 100644 --- a/docs/user/cli/commands/kyverno.md +++ b/docs/user/cli/commands/kyverno.md @@ -46,6 +46,7 @@ kyverno [flags] * [kyverno docs](kyverno_docs.md) - Generates reference documentation. * [kyverno fix](kyverno_fix.md) - Fix inconsistencies and deprecated usage of Kyverno resources. * [kyverno jp](kyverno_jp.md) - Provides a command-line interface to JMESPath, enhanced with Kyverno specific custom functions. +* [kyverno migrate](kyverno_migrate.md) - Migrate one or more resources to the stored version. * [kyverno oci](kyverno_oci.md) - Pulls/pushes images that include policie(s) from/to OCI registries. * [kyverno test](kyverno_test.md) - Run tests from a local filesystem or a remote git repository. * [kyverno version](kyverno_version.md) - Prints the version of Kyverno CLI. diff --git a/docs/user/cli/commands/kyverno_migrate.md b/docs/user/cli/commands/kyverno_migrate.md new file mode 100644 index 0000000000..3e7b1d951f --- /dev/null +++ b/docs/user/cli/commands/kyverno_migrate.md @@ -0,0 +1,50 @@ +## kyverno migrate + +Migrate one or more resources to the stored version. + +### Synopsis + +Migrate one or more resources to the stored version. + +``` +kyverno migrate [flags] +``` + +### Examples + +``` + # Migrate policy exceptions + kyverno migrate --resource policyexceptions.kyverno.io +``` + +### Options + +``` + --context string The name of the kubeconfig context to use + -h, --help help for migrate + --kubeconfig string path to kubeconfig file with authorization and master location information + --resource strings The resource to migrate +``` + +### Options inherited from parent commands + +``` + --add_dir_header If true, adds the file directory to the header of the log messages + --alsologtostderr log to standard error as well as files (no effect when -logtostderr=true) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory (no effect when -logtostderr=true) + --log_file string If non-empty, use this log file (no effect when -logtostderr=true) + --log_file_max_size uint Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + --logtostderr log to standard error instead of files (default true) + --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true) + --skip_headers If true, avoid header prefixes in the log messages + --skip_log_headers If true, avoid headers when opening log files (no effect when -logtostderr=true) + --stderrthreshold severity logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=true) (default 2) + -v, --v Level number for the log level verbosity + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kyverno](kyverno.md) - Kubernetes Native Policy Management. +