From ab68a8d6ed8d879850db9345806a757882320286 Mon Sep 17 00:00:00 2001 From: Mikhail Fedosin Date: Thu, 19 Oct 2023 15:04:33 +0200 Subject: [PATCH] Add plugin skeleton --- cmd/plugin/cmd/delete.go | 146 +++++++++++++++++++++++++++ cmd/plugin/cmd/doc.go | 18 ++++ cmd/plugin/cmd/init.go | 131 +++++++++++++++++++++++++ cmd/plugin/cmd/move.go | 104 ++++++++++++++++++++ cmd/plugin/cmd/root.go | 87 ++++++++--------- cmd/plugin/cmd/upgrade.go | 33 ++++++- cmd/plugin/cmd/upgrade_apply.go | 119 ++++++++++++++++++++++ cmd/plugin/cmd/upgrade_plan.go | 168 ++++++++++++++++++++++++++++++++ cmd/plugin/cmd/utils.go | 53 ++++++++++ cmd/plugin/cmd/version.go | 87 +++++++++++++++++ go.mod | 5 +- go.sum | 2 + 12 files changed, 901 insertions(+), 52 deletions(-) create mode 100644 cmd/plugin/cmd/delete.go create mode 100644 cmd/plugin/cmd/doc.go create mode 100644 cmd/plugin/cmd/init.go create mode 100644 cmd/plugin/cmd/move.go create mode 100644 cmd/plugin/cmd/upgrade_apply.go create mode 100644 cmd/plugin/cmd/upgrade_plan.go create mode 100644 cmd/plugin/cmd/utils.go create mode 100644 cmd/plugin/cmd/version.go diff --git a/cmd/plugin/cmd/delete.go b/cmd/plugin/cmd/delete.go new file mode 100644 index 000000000..1427d70c2 --- /dev/null +++ b/cmd/plugin/cmd/delete.go @@ -0,0 +1,146 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "context" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" +) + +type deleteOptions struct { + kubeconfig string + kubeconfigContext string + coreProvider string + bootstrapProviders []string + controlPlaneProviders []string + infrastructureProviders []string + ipamProviders []string + runtimeExtensionProviders []string + addonProviders []string + includeNamespace bool + includeCRDs bool + deleteAll bool +} + +var deleteOpts = &deleteOptions{} + +var deleteCmd = &cobra.Command{ + Use: "delete [providers]", + GroupID: groupManagement, + Short: "Delete one or more providers from the management cluster", + Long: LongDesc(` + Delete one or more providers from the management cluster.`), + + Example: Examples(` + # Deletes the AWS provider + # Please note that this implies the deletion of all provider components except the hosting namespace + # and the CRDs. + capioperator delete --infrastructure aws + + # Deletes all the providers + # Important! As a consequence of this operation, all the corresponding resources managed by + # Cluster API Providers are orphaned and there might be ongoing costs incurred as a result of this. + capioperator delete --all + + # Delete the AWS infrastructure provider and Core provider. This will leave behind Bootstrap and ControlPlane + # providers + # Important! As a consequence of this operation, all the corresponding resources managed by + # the AWS infrastructure provider and Cluster API Providers are orphaned and there might be + # ongoing costs incurred as a result of this. + capioperator delete --core cluster-api --infrastructure aws + + # Delete the AWS infrastructure provider and related CRDs. Please note that this forces deletion of + # all the related objects (e.g. AWSClusters, AWSMachines etc.). + # Important! As a consequence of this operation, all the corresponding resources managed by + # the AWS infrastructure provider are orphaned and there might be ongoing costs incurred as a result of this. + capioperator delete --infrastructure aws --include-crd + + # Delete the AWS infrastructure provider and its hosting Namespace. Please note that this forces deletion of + # all objects existing in the namespace. + # Important! As a consequence of this operation, all the corresponding resources managed by + # Cluster API Providers are orphaned and there might be ongoing costs incurred as a result of this. + capioperator delete --infrastructure aws --include-namespace + + # Reset the management cluster to its original state + # Important! As a consequence of this operation all the corresponding resources on target clouds + # are "orphaned" and thus there may be ongoing costs incurred as a result of this. + capioperator delete --all --include-crd --include-namespace`), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runDelete() + }, +} + +func init() { + deleteCmd.Flags().StringVar(&deleteOpts.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + deleteCmd.Flags().StringVar(&deleteOpts.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") + + deleteCmd.Flags().BoolVar(&deleteOpts.includeNamespace, "include-namespace", false, + "Forces the deletion of the namespace where the providers are hosted (and of all the contained objects)") + deleteCmd.Flags().BoolVar(&deleteOpts.includeCRDs, "include-crd", false, + "Forces the deletion of the provider's CRDs (and of all the related objects)") + + deleteCmd.Flags().StringVar(&deleteOpts.coreProvider, "core", "", + "Core provider version (e.g. cluster-api:v1.1.5) to delete from the management cluster") + deleteCmd.Flags().StringSliceVarP(&deleteOpts.infrastructureProviders, "infrastructure", "i", nil, + "Infrastructure providers and versions (e.g. aws:v0.5.0) to delete from the management cluster") + deleteCmd.Flags().StringSliceVarP(&deleteOpts.bootstrapProviders, "bootstrap", "b", nil, + "Bootstrap providers and versions (e.g. kubeadm:v1.1.5) to delete from the management cluster") + deleteCmd.Flags().StringSliceVarP(&deleteOpts.controlPlaneProviders, "control-plane", "c", nil, + "ControlPlane providers and versions (e.g. kubeadm:v1.1.5) to delete from the management cluster") + deleteCmd.Flags().StringSliceVar(&deleteOpts.ipamProviders, "ipam", nil, + "IPAM providers and versions (e.g. infoblox:v0.0.1) to delete from the management cluster") + deleteCmd.Flags().StringSliceVar(&deleteOpts.runtimeExtensionProviders, "runtime-extension", nil, + "Runtime extension providers and versions (e.g. test:v0.0.1) to delete from the management cluster") + deleteCmd.Flags().StringSliceVar(&deleteOpts.addonProviders, "addon", nil, + "Add-on providers and versions (e.g. helm:v0.1.0) to delete from the management cluster") + + deleteCmd.Flags().BoolVar(&deleteOpts.deleteAll, "all", false, + "Force deletion of all the providers") + + RootCmd.AddCommand(deleteCmd) +} + +func runDelete() error { + ctx := context.Background() + + hasProviderNames := (deleteOpts.coreProvider != "") || + (len(deleteOpts.bootstrapProviders) > 0) || + (len(deleteOpts.controlPlaneProviders) > 0) || + (len(deleteOpts.infrastructureProviders) > 0) || + (len(deleteOpts.ipamProviders) > 0) || + (len(deleteOpts.runtimeExtensionProviders) > 0) || + (len(deleteOpts.addonProviders) > 0) + + if deleteOpts.deleteAll && hasProviderNames { + return errors.New("The --all flag can't be used in combination with --core, --bootstrap, --control-plane, --infrastructure, --ipam, --extension, --addon") + } + + if !deleteOpts.deleteAll && !hasProviderNames { + return errors.New("At least one of --core, --bootstrap, --control-plane, --infrastructure, --ipam, --extension, --addon should be specified or the --all flag should be set") + } + + return deleteProvider(ctx, deleteOpts) +} + +func deleteProvider(ctx context.Context, opts *deleteOptions) error { + return errors.New("Not implemented") +} diff --git a/cmd/plugin/cmd/doc.go b/cmd/plugin/cmd/doc.go new file mode 100644 index 000000000..72af35499 --- /dev/null +++ b/cmd/plugin/cmd/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2023 The Kubernetes 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 cmd implements capioperator commands. +package cmd diff --git a/cmd/plugin/cmd/init.go b/cmd/plugin/cmd/init.go new file mode 100644 index 000000000..32cf73df8 --- /dev/null +++ b/cmd/plugin/cmd/init.go @@ -0,0 +1,131 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "context" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" +) + +type initOptions struct { + kubeconfig string + kubeconfigContext string + coreProvider string + bootstrapProviders []string + controlPlaneProviders []string + infrastructureProviders []string + ipamProviders []string + runtimeExtensionProviders []string + addonProviders []string + targetNamespace string + validate bool + waitProviders bool + waitProviderTimeout int +} + +var initOpts = &initOptions{} + +var initCmd = &cobra.Command{ + Use: "init", + GroupID: groupManagement, + Short: "Initialize a management cluster", + Long: LongDesc(` + Initialize a management cluster. + + Installs Cluster API operator, core components, the kubeadm bootstrap provider, + and the selected bootstrap and infrastructure providers. + + The management cluster must be an existing Kubernetes cluster, make sure + to have enough privileges to install the desired components. + + Some providers require secrets to be created before running 'capioperator init'. + Refer to the provider documentation, or use 'clusterctl config provider [name]' to get a list of required variables. + + See https://cluster-api.sigs.k8s.io and https://github.com/kubernetes-sigs/cluster-api-operator/blob/main/docs/README.md for more details.`), + + Example: Examples(` + # Initialize CAPI operator only without installing any providers. + # capioperator init + + # Initialize a management cluster, by installing the given infrastructure provider. + # + # Note: when this command is executed on an empty management cluster, + # it automatically triggers the installation of the Cluster API core provider. + capioperator init --infrastructure=aws + + # Initialize a management cluster with a specific version of the given infrastructure provider. + capioperator init --infrastructure=aws:v0.4.1 + + # Initialize a management cluster with a specific version and namespace of the given infrastructure provider. + capioperator init --infrastructure=custom-namespace:aws:v0.4.1 + + # Initialize a management cluster with a custom kubeconfig path and the given infrastructure provider. + capioperator init --kubeconfig=foo.yaml --infrastructure=aws + + # Initialize a management cluster with multiple infrastructure providers. + capioperator init --infrastructure=aws;vsphere + + # Initialize a management cluster with a custom target namespace for the operator. + capioperator init --infrastructure aws --target-namespace foo`), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runInit() + }, +} + +func init() { + initCmd.PersistentFlags().StringVar(&initOpts.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.") + initCmd.PersistentFlags().StringVar(&initOpts.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") + initCmd.PersistentFlags().StringVar(&initOpts.coreProvider, "core", "", + "Core provider version (e.g. cluster-api:v1.1.5) to add to the management cluster. If unspecified, Cluster API's latest release is used.") + initCmd.PersistentFlags().StringSliceVarP(&initOpts.infrastructureProviders, "infrastructure", "i", nil, + "Infrastructure providers and versions (e.g. aws:v0.5.0) to add to the management cluster.") + initCmd.PersistentFlags().StringSliceVarP(&initOpts.bootstrapProviders, "bootstrap", "b", nil, + "Bootstrap providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, Kubeadm bootstrap provider's latest release is used.") + initCmd.PersistentFlags().StringSliceVarP(&initOpts.controlPlaneProviders, "control-plane", "c", nil, + "Control plane providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, the Kubeadm control plane provider's latest release is used.") + initCmd.PersistentFlags().StringSliceVar(&initOpts.ipamProviders, "ipam", nil, + "IPAM providers and versions (e.g. infoblox:v0.0.1) to add to the management cluster.") + initCmd.PersistentFlags().StringSliceVar(&initOpts.runtimeExtensionProviders, "runtime-extension", nil, + "Runtime extension providers and versions (e.g. test:v0.0.1) to add to the management cluster.") + initCmd.PersistentFlags().StringSliceVar(&initOpts.addonProviders, "addon", nil, + "Add-on providers and versions (e.g. helm:v0.1.0) to add to the management cluster.") + initCmd.Flags().StringVarP(&initOpts.targetNamespace, "target-namespace", "n", "capi-operator-system", + "The target namespace where the operator should be deployed. If unspecified, the 'capi-operator-system' namespace is used.") + initCmd.Flags().BoolVar(&initOpts.waitProviders, "wait-providers", false, + "Wait for providers to be installed.") + initCmd.Flags().IntVar(&initOpts.waitProviderTimeout, "wait-provider-timeout", 5*60, + "Wait timeout per provider installation in seconds. This value is ignored if --wait-providers is false") + initCmd.Flags().BoolVar(&initOpts.validate, "validate", true, + "If true, capioperator will validate that the deployments will succeed on the management cluster.") + + RootCmd.AddCommand(initCmd) +} + +func runInit() error { + ctx := context.Background() + + return initProvider(ctx, initOpts) +} + +func initProvider(ctx context.Context, opts *initOptions) error { + return errors.New("Not implemented") +} diff --git a/cmd/plugin/cmd/move.go b/cmd/plugin/cmd/move.go new file mode 100644 index 000000000..3630f32c2 --- /dev/null +++ b/cmd/plugin/cmd/move.go @@ -0,0 +1,104 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "context" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" +) + +type moveOptions struct { + fromKubeconfig string + fromKubeconfigContext string + toKubeconfig string + toKubeconfigContext string + namespace string + fromDirectory string + toDirectory string + dryRun bool +} + +var moveOpts = &moveOptions{} + +var moveCmd = &cobra.Command{ + Use: "move", + GroupID: groupManagement, + Short: "Move Cluster API objects and all dependencies between management clusters", + Long: LongDesc(` + Move Cluster API objects and all dependencies between management clusters. + + Note: The destination cluster MUST have the required provider components installed.`), + + Example: Examples(` + Move Cluster API objects and all dependencies between management clusters. + capioperator move --to-kubeconfig=target-kubeconfig.yaml + + Write Cluster API objects and all dependencies from a management cluster to directory. + capioperator move --to-directory /tmp/backup-directory + + Read Cluster API objects and all dependencies from a directory into a management cluster. + capioperator move --from-directory /tmp/backup-directory + `), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runMove() + }, +} + +func init() { + moveCmd.Flags().StringVar(&moveOpts.fromKubeconfig, "kubeconfig", "", + "Path to the kubeconfig file for the source management cluster. If unspecified, default discovery rules apply.") + moveCmd.Flags().StringVar(&moveOpts.toKubeconfig, "to-kubeconfig", "", + "Path to the kubeconfig file to use for the destination management cluster.") + moveCmd.Flags().StringVar(&moveOpts.fromKubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file for the source management cluster. If empty, current context will be used.") + moveCmd.Flags().StringVar(&moveOpts.toKubeconfigContext, "to-kubeconfig-context", "", + "Context to be used within the kubeconfig file for the destination management cluster. If empty, current context will be used.") + moveCmd.Flags().StringVarP(&moveOpts.namespace, "namespace", "n", "", + "The namespace where the workload cluster is hosted. If unspecified, the current context's namespace is used.") + moveCmd.Flags().BoolVar(&moveOpts.dryRun, "dry-run", false, + "Enable dry run, don't really perform the move actions") + moveCmd.Flags().StringVar(&moveOpts.toDirectory, "to-directory", "", + "Write Cluster API objects and all dependencies from a management cluster to directory.") + moveCmd.Flags().StringVar(&moveOpts.fromDirectory, "from-directory", "", + "Read Cluster API objects and all dependencies from a directory into a management cluster.") + + moveCmd.MarkFlagsMutuallyExclusive("to-directory", "to-kubeconfig") + moveCmd.MarkFlagsMutuallyExclusive("from-directory", "to-directory") + moveCmd.MarkFlagsMutuallyExclusive("from-directory", "kubeconfig") + + RootCmd.AddCommand(moveCmd) +} + +func runMove() error { + ctx := context.Background() + + if moveOpts.toDirectory == "" && + moveOpts.fromDirectory == "" && + moveOpts.toKubeconfig == "" && + !moveOpts.dryRun { + return errors.New("please specify a target cluster using the --to-kubeconfig flag when not using --dry-run, --to-directory or --from-directory") + } + + return moveProvider(ctx, moveOpts) +} + +func moveProvider(ctx context.Context, opts *moveOptions) error { + return errors.New("Not implemented") +} diff --git a/cmd/plugin/cmd/root.go b/cmd/plugin/cmd/root.go index 47abdf118..9efb189e7 100644 --- a/cmd/plugin/cmd/root.go +++ b/cmd/plugin/cmd/root.go @@ -17,48 +17,47 @@ limitations under the License. package cmd import ( + "errors" "flag" - "fmt" "os" - "strconv" "strings" "github.com/MakeNowJust/heredoc" - "github.com/pkg/errors" + goerrors "github.com/go-errors/errors" "github.com/spf13/cobra" - - configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" - logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" ) -type stackTracer interface { - StackTrace() errors.StackTrace -} - -var ( - cfgFile string - verbosity *int +const ( + groupDebug = "group-debug" + groupManagement = "group-management" + groupOther = "group-other" ) -// RootCmd is operator root CLI command. +var verbosity *int + +// RootCmd is capioperator root CLI command. var RootCmd = &cobra.Command{ - Use: "operator", - Short: "clusterctl plugin for leveraging Cluster API Operator.", + Use: "capioperator", + SilenceUsage: true, + Short: "capioperator controls the lifecycle of a Cluster API management cluster", Long: LongDesc(` - Use this clusterctl plugin to bootstrap a management cluster for Cluster API with the Cluster API Operator.`), + Get started with Cluster API using capioperator to create a management cluster, + install providers, and create templates for your workload cluster.`), + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + return nil + }, } // Execute executes the root command. func Execute() { if err := RootCmd.Execute(); err != nil { if verbosity != nil && *verbosity >= 5 { - if err, ok := err.(stackTracer); ok { //nolint:errorlint - for _, f := range err.StackTrace() { - fmt.Fprintf(os.Stderr, "%+s:%d\n", f, f) - } + var stackErr *goerrors.Error + if errors.As(err, &stackErr) { + stackErr.ErrorStack() } } - + // TODO: print cmd help if validation error os.Exit(1) } } @@ -66,34 +65,26 @@ func Execute() { func init() { flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) - verbosity = flag.CommandLine.Int("v", 0, "Set the log level verbosity. This overrides the CLUSTERCTL_LOG_LEVEL environment variable.") + verbosity = flag.CommandLine.Int("v", 0, "Set the log level verbosity. This overrides the CAPIOPERATOR_LOG_LEVEL environment variable.") RootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) - RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", - "Path to clusterctl configuration (default is `$XDG_CONFIG_HOME/cluster-api/clusterctl.yaml`) or to a remote location (i.e. https://example.com/clusterctl.yaml)") - cobra.OnInitialize(initConfig) -} - -func initConfig() { - // check if the CLUSTERCTL_LOG_LEVEL was set via env var or in the config file - if *verbosity == 0 { //nolint:nestif - configClient, err := configclient.New(cfgFile) - if err == nil { - v, err := configClient.Variables().Get("CLUSTERCTL_LOG_LEVEL") - if err == nil && v != "" { - verbosityFromEnv, err := strconv.Atoi(v) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to convert CLUSTERCTL_LOG_LEVEL string to an int. err=%s\n", err.Error()) - os.Exit(1) - } - - verbosity = &verbosityFromEnv - } - } - } - - logf.SetLogger(logf.NewLogger(logf.WithThreshold(verbosity))) + RootCmd.AddGroup( + &cobra.Group{ + ID: groupManagement, + Title: "Cluster Management Commands:", + }, + &cobra.Group{ + ID: groupDebug, + Title: "Troubleshooting and Debugging Commands:", + }, + &cobra.Group{ + ID: groupOther, + Title: "Other Commands:", + }) + + RootCmd.SetHelpCommandGroupID(groupOther) + RootCmd.SetCompletionCommandGroupID(groupOther) } const indentation = ` ` @@ -122,11 +113,13 @@ type normalizer struct { func (s normalizer) heredoc() normalizer { s.string = heredoc.Doc(s.string) + return s } func (s normalizer) trim() normalizer { s.string = strings.TrimSpace(s.string) + return s } diff --git a/cmd/plugin/cmd/upgrade.go b/cmd/plugin/cmd/upgrade.go index 54da79d6e..19926fa9a 100644 --- a/cmd/plugin/cmd/upgrade.go +++ b/cmd/plugin/cmd/upgrade.go @@ -17,18 +17,45 @@ limitations under the License. package cmd import ( + "sort" + "github.com/spf13/cobra" ) var upgradeCmd = &cobra.Command{ - Use: "upgrade", - Short: "Upgrade core and provider components in a management cluster using the Cluster API Operator.", - Args: cobra.NoArgs, + Use: "upgrade", + GroupID: groupManagement, + Short: "Upgrade core and provider components in a management cluster", + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, } func init() { + upgradeCmd.AddCommand(upgradePlanCmd) + upgradeCmd.AddCommand(upgradeApplyCmd) RootCmd.AddCommand(upgradeCmd) } + +func sortUpgradeItems(plan upgradePlan) { + sort.Slice(plan.Providers, func(i, j int) bool { + return plan.Providers[i].GetType() < plan.Providers[j].GetType() || + (plan.Providers[i].GetType() == plan.Providers[j].GetType() && plan.Providers[i].GetName() < plan.Providers[j].GetName()) || + (plan.Providers[i].GetType() == plan.Providers[j].GetType() && plan.Providers[i].GetName() == plan.Providers[j].GetName() && plan.Providers[i].GetNamespace() < plan.Providers[j].GetNamespace()) + }) +} + +func sortUpgradePlans(upgradePlans []upgradePlan) { + sort.Slice(upgradePlans, func(i, j int) bool { + return upgradePlans[i].Contract < upgradePlans[j].Contract + }) +} + +func prettifyTargetVersion(version string) string { + if version == "" { + return "Already up to date" + } + + return version +} diff --git a/cmd/plugin/cmd/upgrade_apply.go b/cmd/plugin/cmd/upgrade_apply.go new file mode 100644 index 000000000..d6fa1a339 --- /dev/null +++ b/cmd/plugin/cmd/upgrade_apply.go @@ -0,0 +1,119 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "context" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" +) + +type upgradeApplyOptions struct { + kubeconfig string + kubeconfigContext string + contract string + coreProvider string + bootstrapProviders []string + controlPlaneProviders []string + infrastructureProviders []string + ipamProviders []string + runtimeExtensionProviders []string + addonProviders []string + waitProviders bool + waitProviderTimeout int +} + +var upgradeApplyOpts = &upgradeApplyOptions{} + +var upgradeApplyCmd = &cobra.Command{ + Use: "apply", + Short: "Apply new versions of Cluster API core and providers in a management cluster", + Long: LongDesc(` + The upgrade apply command applies new versions of Cluster API providers as defined by capioperator upgrade plan. + + New version should be applied ensuring all the providers uses the same cluster API version + in order to guarantee the proper functioning of the management cluster. + + Specifying the provider using namespace/name:version is deprecated and will be dropped in a future release.`), + + Example: Examples(` + # Upgrades all the providers in the management cluster to the latest version available which is compliant + # to the v1alpha4 API Version of Cluster API (contract). + capioperator upgrade apply --contract v1alpha4 + + # Upgrades only the aws provider to the v2.0.1 version. + capioperator upgrade apply --infrastructure aws:v2.0.1`), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runUpgradeApply() + }, +} + +func init() { + upgradeApplyCmd.Flags().StringVar(&upgradeApplyOpts.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + upgradeApplyCmd.Flags().StringVar(&upgradeApplyOpts.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") + upgradeApplyCmd.Flags().StringVar(&upgradeApplyOpts.contract, "contract", "", + "The API Version of Cluster API (contract, e.g. v1alpha4) the management cluster should upgrade to") + + upgradeApplyCmd.Flags().StringVar(&upgradeApplyOpts.coreProvider, "core", "", + "Core provider instance version (e.g. cluster-api:v1.1.5) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().StringSliceVarP(&upgradeApplyOpts.infrastructureProviders, "infrastructure", "i", nil, + "Infrastructure providers instance and versions (e.g. aws:v2.0.1) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().StringSliceVarP(&upgradeApplyOpts.bootstrapProviders, "bootstrap", "b", nil, + "Bootstrap providers instance and versions (e.g. kubeadm:v1.1.5) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().StringSliceVarP(&upgradeApplyOpts.controlPlaneProviders, "control-plane", "c", nil, + "ControlPlane providers instance and versions (e.g. kubeadm:v1.1.5) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().StringSliceVar(&upgradeApplyOpts.ipamProviders, "ipam", nil, + "IPAM providers and versions (e.g. infoblox:v0.0.1) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().StringSliceVar(&upgradeApplyOpts.runtimeExtensionProviders, "runtime-extension", nil, + "Runtime extension providers and versions (e.g. test:v0.0.1) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().StringSliceVar(&upgradeApplyOpts.addonProviders, "addon", nil, + "Add-on providers and versions (e.g. helm:v0.1.0) to upgrade to. This flag can be used as alternative to --contract.") + upgradeApplyCmd.Flags().BoolVar(&upgradeApplyOpts.waitProviders, "wait-providers", false, + "Wait for providers to be upgraded.") + upgradeApplyCmd.Flags().IntVar(&upgradeApplyOpts.waitProviderTimeout, "wait-provider-timeout", 5*60, + "Wait timeout per provider upgrade in seconds. This value is ignored if --wait-providers is false") +} + +func runUpgradeApply() error { + ctx := context.Background() + + hasProviderNames := (upgradeApplyOpts.coreProvider != "") || + (len(upgradeApplyOpts.bootstrapProviders) > 0) || + (len(upgradeApplyOpts.controlPlaneProviders) > 0) || + (len(upgradeApplyOpts.infrastructureProviders) > 0) || + (len(upgradeApplyOpts.ipamProviders) > 0) || + (len(upgradeApplyOpts.runtimeExtensionProviders) > 0) || + (len(upgradeApplyOpts.addonProviders) > 0) + + if upgradeApplyOpts.contract == "" && !hasProviderNames { + return errors.New("Either the --contract flag or at least one of the following flags has to be set: --core, --bootstrap, --control-plane, --infrastructure, --ipam, --extension, --addon") + } + + if upgradeApplyOpts.contract != "" && hasProviderNames { + return errors.New("The --contract flag can't be used in combination with --core, --bootstrap, --control-plane, --infrastructure, --ipam, --extension, --addon") + } + + return upgradeProvider(ctx, upgradeApplyOpts) +} + +func upgradeProvider(ctx context.Context, opts *upgradeApplyOptions) error { + return errors.New("Not implemented") +} diff --git a/cmd/plugin/cmd/upgrade_plan.go b/cmd/plugin/cmd/upgrade_plan.go new file mode 100644 index 000000000..731e03675 --- /dev/null +++ b/cmd/plugin/cmd/upgrade_plan.go @@ -0,0 +1,168 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "context" + "fmt" + "os" + "text/tabwriter" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + + "sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider" +) + +type upgradePlanOptions struct { + kubeconfig string + kubeconfigContext string +} + +// certManagerUpgradePlan defines the upgrade plan if cert-manager needs to be +// upgraded to a different version. +type certManagerUpgradePlan struct { + ExternallyManaged bool + From, To string + ShouldUpgrade bool +} + +// upgradePlan defines a list of possible upgrade targets for a management cluster. +type upgradePlan struct { + Contract string + Providers []upgradeItem +} + +// upgradeItem defines a possible upgrade target for a provider in the management cluster. +type upgradeItem struct { + genericprovider.GenericProvider + NextVersion string +} + +var upgradePlanOpts = &upgradePlanOptions{} + +var upgradePlanCmd = &cobra.Command{ + Use: "plan", + Short: "Provide a list of recommended target versions for upgrading Cluster API providers in a management cluster", + Long: LongDesc(` + The upgrade plan command provides a list of recommended target versions for upgrading the + Cluster API providers in a management cluster. + + All the providers should be supporting the same API Version of Cluster API (contract) in order + to guarantee the proper functioning of the management cluster. + + Then, for each provider, the following upgrade options are provided: + - The latest patch release for the current API Version of Cluster API (contract). + - The latest patch release for the next API Version of Cluster API (contract), if available.`), + + Example: Examples(` + # Gets the recommended target versions for upgrading Cluster API providers. + capioperator upgrade plan`), + + RunE: func(cmd *cobra.Command, args []string) error { + return runUpgradePlan() + }, +} + +func init() { + upgradePlanCmd.Flags().StringVar(&upgradePlanOpts.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply.") + upgradePlanCmd.Flags().StringVar(&upgradePlanOpts.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") +} + +func runUpgradePlan() error { + ctx := context.Background() + + certManUpgradePlan, err := planCertManagerUpgrade(ctx, upgradePlanOpts) + if err != nil { + return err + } + + if !certManUpgradePlan.ExternallyManaged { + if certManUpgradePlan.ShouldUpgrade { + fmt.Printf("Cert-Manager will be upgraded from %q to %q\n\n", certManUpgradePlan.From, certManUpgradePlan.To) + } else { + fmt.Printf("Cert-Manager is already up to date\n\n") + } + } + + upgradePlans, err := planUpgrade(ctx, upgradePlanOpts) + if err != nil { + return err + } + + if len(upgradePlans) == 0 { + fmt.Println("There are no providers in the cluster. Please use capioperator init to initialize a Cluster API management cluster.") + return nil + } + + // ensure upgrade plans are sorted consistently (by CoreProvider.Namespace, Contract). + sortUpgradePlans(upgradePlans) + + for _, plan := range upgradePlans { + // ensure provider are sorted consistently (by Type, Name, Namespace). + sortUpgradeItems(plan) + + upgradeAvailable := false + + fmt.Printf("\nLatest release available for the %s API Version of Cluster API (contract):\n\n", plan.Contract) + + w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0) + + fmt.Fprintln(w, "NAME\tNAMESPACE\tTYPE\tCURRENT VERSION\tNEXT VERSION") + + for _, upgradeItem := range plan.Providers { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", upgradeItem.GetName(), upgradeItem.GetNamespace(), upgradeItem.GetType(), upgradeItem.GetSpec().Version, prettifyTargetVersion(upgradeItem.NextVersion)) + + if upgradeItem.NextVersion != "" { + upgradeAvailable = true + } + } + + if err := w.Flush(); err != nil { + return err + } + + fmt.Println("") + + if upgradeAvailable { + if plan.Contract == clusterv1.GroupVersion.Version { + fmt.Println("You can now apply the upgrade by executing the following command:") + fmt.Println("") + fmt.Printf("capioperator upgrade apply --contract %s\n", plan.Contract) + } else { + fmt.Printf("The current version of capioperator could not upgrade to %s contract (only %s supported).\n", plan.Contract, clusterv1.GroupVersion.Version) + } + } else { + fmt.Println("You are already up to date!") + } + + fmt.Println("") + } + + return nil +} + +func planCertManagerUpgrade(ctx context.Context, opts *upgradePlanOptions) (certManagerUpgradePlan, error) { + return certManagerUpgradePlan{}, errors.New("Not implemented") +} + +func planUpgrade(ctx context.Context, opts *upgradePlanOptions) ([]upgradePlan, error) { + return nil, errors.New("Not implemented") +} diff --git a/cmd/plugin/cmd/utils.go b/cmd/plugin/cmd/utils.go new file mode 100644 index 000000000..cd4b07092 --- /dev/null +++ b/cmd/plugin/cmd/utils.go @@ -0,0 +1,53 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" +) + +// CreateKubeClient creates a kubernetes client from provided kubeconfig and kubecontext. +func CreateKubeClient(kubeconfigPath, kubeconfigContext string) (ctrlclient.Client, error) { + // Use specified kubeconfig path and context + config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, + &clientcmd.ConfigOverrides{ + CurrentContext: kubeconfigContext, + }).ClientConfig() + if err != nil { + return nil, fmt.Errorf("error loading client config: %w", err) + } + + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(operatorv1.AddToScheme(scheme)) + + client, err := ctrlclient.New(config, ctrlclient.Options{Scheme: scheme}) + if err != nil { + return nil, fmt.Errorf("error creating client: %w", err) + } + + return client, nil +} diff --git a/cmd/plugin/cmd/version.go b/cmd/plugin/cmd/version.go new file mode 100644 index 000000000..967c2b2ef --- /dev/null +++ b/cmd/plugin/cmd/version.go @@ -0,0 +1,87 @@ +/* +Copyright 2023 The Kubernetes 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 cmd + +import ( + "encoding/json" + "fmt" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "sigs.k8s.io/cluster-api/version" +) + +// Version provides the version information of CAPI operator. +type Version struct { + ClientVersion *version.Info `json:"capioperator"` +} + +type versionOptions struct { + output string +} + +var vo = &versionOptions{} + +var versionCmd = &cobra.Command{ + Use: "version", + GroupID: groupOther, + Short: "Print version of CAPI operator", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runVersion() + }, +} + +func init() { + versionCmd.Flags().StringVarP(&vo.output, "output", "o", "", "Output format; available options are 'yaml', 'json' and 'short'") + + RootCmd.AddCommand(versionCmd) +} + +func runVersion() error { + clientVersion := version.Get() + v := Version{ + ClientVersion: &clientVersion, + } + + switch vo.output { + case "": + fmt.Printf("CAPI operator version: %#v\n", v.ClientVersion) + case "short": + fmt.Printf("%s\n", v.ClientVersion.GitVersion) + case "yaml": + y, err := yaml.Marshal(&v) + if err != nil { + return err + } + + fmt.Print(string(y)) + case "json": + y, err := json.MarshalIndent(&v, "", " ") + if err != nil { + return err + } + + fmt.Println(string(y)) + default: + return errors.Errorf("invalid output format: %s", vo.output) + } + + return nil +} diff --git a/go.mod b/go.mod index b4825ab5d..06a650cc9 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ replace sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v1.5.1 require ( github.com/MakeNowJust/heredoc v1.0.0 + github.com/go-errors/errors v1.4.2 github.com/google/go-cmp v0.5.9 github.com/google/go-github/v52 v52.0.0 github.com/google/gofuzz v1.2.0 github.com/onsi/gomega v1.28.0 - github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 golang.org/x/oauth2 v0.11.0 @@ -23,6 +23,7 @@ require ( k8s.io/utils v0.0.0-20230209194617-a36077c30491 sigs.k8s.io/cluster-api v1.5.1 sigs.k8s.io/controller-runtime v0.15.2 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -77,6 +78,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect @@ -109,5 +111,4 @@ require ( k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 5e9674c1f..417b23c8c 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=