From 8b61e231901f7a8bfdc5fa5fdc95e9f77afa906d Mon Sep 17 00:00:00 2001 From: Mikhail Fedosin Date: Tue, 5 Dec 2023 12:20:09 +0100 Subject: [PATCH 1/3] Implement "upgrade plan" support --- cmd/plugin/cmd/init.go | 3 - cmd/plugin/cmd/upgrade.go | 12 +- cmd/plugin/cmd/upgrade_plan.go | 378 +++++++++++++++++++++++++++++---- cmd/plugin/cmd/utils.go | 6 + 4 files changed, 346 insertions(+), 53 deletions(-) diff --git a/cmd/plugin/cmd/init.go b/cmd/plugin/cmd/init.go index e410a4212..60b8b2e04 100644 --- a/cmd/plugin/cmd/init.go +++ b/cmd/plugin/cmd/init.go @@ -59,9 +59,6 @@ type initOptions struct { const ( capiOperatorProviderName = "capi-operator" - // We have to specify a version here, because if we set "latest", clusterctl libs will try to fetch metadata.yaml file for the latest - // release and fail since CAPI operator doesn't provide this file. - capiOperatorManifestsURL = "https://github.com/kubernetes-sigs/cluster-api-operator/releases/v0.1.0/operator-components.yaml" ) var initOpts = &initOptions{} diff --git a/cmd/plugin/cmd/upgrade.go b/cmd/plugin/cmd/upgrade.go index 19926fa9a..3c7d72df3 100644 --- a/cmd/plugin/cmd/upgrade.go +++ b/cmd/plugin/cmd/upgrade.go @@ -40,15 +40,9 @@ func init() { 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 + return plan.Providers[i].Type < plan.Providers[j].Type || + (plan.Providers[i].Type == plan.Providers[j].Type && plan.Providers[i].Name < plan.Providers[j].Name) || + (plan.Providers[i].Type == plan.Providers[j].Type && plan.Providers[i].Name == plan.Providers[j].Name && plan.Providers[i].Namespace < plan.Providers[j].Namespace) }) } diff --git a/cmd/plugin/cmd/upgrade_plan.go b/cmd/plugin/cmd/upgrade_plan.go index c594e8187..5f0ab0b6e 100644 --- a/cmd/plugin/cmd/upgrade_plan.go +++ b/cmd/plugin/cmd/upgrade_plan.go @@ -21,13 +21,19 @@ import ( "context" "fmt" "os" + "strings" "text/tabwriter" - "github.com/go-errors/errors" "github.com/spf13/cobra" + appsv1 "k8s.io/api/apps/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" + "sigs.k8s.io/cluster-api-operator/util" ) type upgradePlanOptions struct { @@ -43,16 +49,39 @@ type certManagerUpgradePlan struct { ShouldUpgrade bool } +// capiOperatorUpgradePlan defines the upgrade plan if CAPI operator needs to be +// upgraded to a different version. +type capiOperatorUpgradePlan 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 } +type providerSource string + +type providerSourceType string + +var ( + providerSourceTypeBuiltin providerSourceType = "builtin" + providerSourceTypeCustomURL providerSourceType = "custom-url" + providerSourceTypeConfigMap providerSourceType = "config-map" +) + // upgradeItem defines a possible upgrade target for a provider in the management cluster. type upgradeItem struct { - operatorv1.GenericProvider - NextVersion string + Name string + Namespace string + Type string + Source providerSource + SourceType providerSourceType + CurrentVersion string + NextVersion string } var upgradePlanOpts = &upgradePlanOptions{} @@ -90,6 +119,10 @@ func init() { func runUpgradePlan() error { ctx := context.Background() + if upgradePlanOpts.kubeconfig == "" { + upgradePlanOpts.kubeconfig = GetKubeconfigLocation() + } + certManUpgradePlan, err := planCertManagerUpgrade(ctx, upgradePlanOpts) if err != nil { return err @@ -97,73 +130,336 @@ func runUpgradePlan() error { if !certManUpgradePlan.ExternallyManaged { if certManUpgradePlan.ShouldUpgrade { - fmt.Printf("Cert-Manager will be upgraded from %q to %q\n\n", certManUpgradePlan.From, certManUpgradePlan.To) + log.Info("Cert-Manager can be upgraded", "Installed Version", certManUpgradePlan.From, "Available Version", certManUpgradePlan.To) } else { - fmt.Printf("Cert-Manager is already up to date\n\n") + log.Info("Cert-Manager is already up to date", "Installed Version", certManUpgradePlan.From) } + } else { + log.Info("There are no managed Cert-Manager installations found") } - upgradePlans, err := planUpgrade(ctx, upgradePlanOpts) + capiOperatorUpgradePlan, err := planCAPIOperatorUpgrade(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 + if capiOperatorUpgradePlan.ShouldUpgrade { + log.Info("CAPI operator can be upgraded", "Installed Version", capiOperatorUpgradePlan.From, "Available Version", capiOperatorUpgradePlan.To) + } else { + log.Info("CAPI operator is already up to date", "Installed Version", capiOperatorUpgradePlan.From) + } + + if capiOperatorUpgradePlan.ExternallyManaged { + log.Info("CAPI operator is not managed by the plugin and won't be modified during upgrade") + } + + upgradePlan, err := planUpgrade(ctx) + if err != nil { + return err } - // ensure upgrade plans are sorted consistently (by CoreProvider.Namespace, Contract). - sortUpgradePlans(upgradePlans) + if len(upgradePlan.Providers) == 0 { + log.Info("There are no providers in the cluster. Please use capioperator init to initialize a Cluster API management cluster.") + return nil + } - for _, plan := range upgradePlans { - // ensure provider are sorted consistently (by Type, Name, Namespace). - sortUpgradeItems(plan) + // ensure provider are sorted consistently (by Type, Name, Namespace). + sortUpgradeItems(upgradePlan) - upgradeAvailable := false + upgradeAvailable := false - fmt.Printf("\nLatest release available for the %s API Version of Cluster API (contract):\n\n", plan.Contract) + fmt.Printf("\nLatest release available for the %s API Version of Cluster API (contract):\n\n", upgradePlan.Contract) - w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0) + w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0) - fmt.Fprintln(w, "NAME\tNAMESPACE\tTYPE\tCURRENT VERSION\tNEXT VERSION") + 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)) + for _, upgradeItem := range upgradePlan.Providers { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", upgradeItem.Name, upgradeItem.Namespace, upgradeItem.Type, upgradeItem.CurrentVersion, prettifyTargetVersion(upgradeItem.NextVersion)) - if upgradeItem.NextVersion != "" { - upgradeAvailable = true - } + if upgradeItem.NextVersion != "" { + upgradeAvailable = true } + } - if err := w.Flush(); err != nil { - return err - } + if err := w.Flush(); err != nil { + return err + } - fmt.Println("") + 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) - } + if upgradeAvailable { + if upgradePlan.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", upgradePlan.Contract) } else { - fmt.Println("You are already up to date!") + fmt.Printf("The current version of capioperator could not upgrade to %s contract (only %s supported).\n", upgradePlan.Contract, clusterv1.GroupVersion.Version) } - - fmt.Println("") + } 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") + upgradePlan := certManagerUpgradePlan{} + + configClient, err := configclient.New(ctx, "") + if err != nil { + return upgradePlan, fmt.Errorf("cannot create config client: %w", err) + } + + clusterKubeconfig := cluster.Kubeconfig{ + Path: opts.kubeconfig, + Context: opts.kubeconfigContext, + } + + clusterClient := cluster.New(clusterKubeconfig, configClient) + + clusterctlUpgradePlan, err := clusterClient.CertManager().PlanUpgrade(ctx) + if err != nil { + return upgradePlan, fmt.Errorf("cannot create upgrade plan for cert-manager: %w", err) + } + + return certManagerUpgradePlan{ + ExternallyManaged: clusterctlUpgradePlan.ExternallyManaged, + From: clusterctlUpgradePlan.From, + To: clusterctlUpgradePlan.To, + ShouldUpgrade: clusterctlUpgradePlan.ShouldUpgrade, + }, nil +} + +func planCAPIOperatorUpgrade(ctx context.Context, opts *upgradePlanOptions) (capiOperatorUpgradePlan, error) { + upgradePlan := capiOperatorUpgradePlan{} + + log.Info("Checking CAPI Operator version...") + + client, err := CreateKubeClient(opts.kubeconfig, opts.kubeconfigContext) + if err != nil { + return upgradePlan, fmt.Errorf("cannot create a client: %w", err) + } + + capiOperatorDeployment, err := GetDeploymentByLabels(ctx, client, capiOperatorLabels) + if err != nil { + return upgradePlan, fmt.Errorf("cannot get CAPI operator deployment: %w", err) + } + + for _, container := range capiOperatorDeployment.Spec.Template.Spec.Containers { + if container.Name == "manager" { + parts := strings.Split(container.Image, ":") + upgradePlan.From = parts[len(parts)-1] + + log.Info("Found CAPI Operator deployment", "Version", upgradePlan.From) + + break + } + } + + log.Info("Fetching data about all CAPI Operator releases.") + + configClient, err := configclient.New(ctx, "") + if err != nil { + return upgradePlan, fmt.Errorf("cannot create config client: %w", err) + } + + providerConfig := configclient.NewProvider(capiOperatorProviderName, capiOperatorManifestsURL, clusterctlv1.ProviderTypeUnknown) + + repo, err := util.RepositoryFactory(ctx, providerConfig, configClient.Variables()) + if err != nil { + return upgradePlan, fmt.Errorf("cannot create repository: %w", err) + } + + latestReleasedVersion, err := GetLatestRelease(ctx, repo) + if err != nil { + return upgradePlan, fmt.Errorf("cannot get latest release: %w", err) + } + + log.Info("Found latest available CAPI Operator release", "Latest Release", latestReleasedVersion) + + upgradePlan.To = latestReleasedVersion + + upgradePlan.ShouldUpgrade = upgradePlan.From != upgradePlan.To + + upgradePlan.ExternallyManaged = isCAPIOperatorExternallyManaged(capiOperatorDeployment) + + return upgradePlan, nil +} + +// isCAPIOperatorExternallyManaged returns true if the CAPI operator is not managed by the plugin. +func isCAPIOperatorExternallyManaged(deployment *appsv1.Deployment) bool { + return deployment.Labels[clusterv1.ProviderNameLabel] != capiOperatorProviderName +} + +func planUpgrade(ctx context.Context) (upgradePlan, error) { + if initOpts.kubeconfig == "" { + initOpts.kubeconfig = GetKubeconfigLocation() + } + + genericProviders, contract, err := getInstalledProviders(ctx) + if err != nil { + return upgradePlan{}, fmt.Errorf("cannot get installed providers: %w", err) + } + + upgradeItems := []upgradeItem{} + + for _, genericProvider := range genericProviders { + providerFetchSource, providerSourceType, err := getProviderFetchConfig(ctx, genericProvider) + if err != nil { + return upgradePlan{}, fmt.Errorf("cannot get provider fetch URL: %w", err) + } + + // TODO: ignore configmap source type for now. + if providerSourceType == providerSourceTypeConfigMap { + continue + } + + configClient, err := configclient.New(ctx, "") + if err != nil { + return upgradePlan{}, fmt.Errorf("cannot create config client: %w", err) + } + + providerConfig := configclient.NewProvider(capiOperatorProviderName, string(providerFetchSource), clusterctlv1.ProviderTypeUnknown) + + repo, err := util.RepositoryFactory(ctx, providerConfig, configClient.Variables()) + if err != nil { + return upgradePlan{}, fmt.Errorf("cannot create repository: %w", err) + } + + upgradeItems = append(upgradeItems, upgradeItem{ + Name: genericProvider.GetName(), + Namespace: genericProvider.GetNamespace(), + Type: genericProvider.GetType(), + CurrentVersion: genericProvider.GetSpec().Version, + NextVersion: repo.DefaultVersion(), + Source: providerFetchSource, + SourceType: providerSourceType, + }) + } + + return upgradePlan{Contract: contract, Providers: upgradeItems}, nil } -func planUpgrade(ctx context.Context, opts *upgradePlanOptions) ([]upgradePlan, error) { - return nil, errors.New("Not implemented") +func getInstalledProviders(ctx context.Context) ([]operatorv1.GenericProvider, string, error) { + client, err := CreateKubeClient(initOpts.kubeconfig, initOpts.kubeconfigContext) + if err != nil { + return nil, "", fmt.Errorf("cannot create a client: %w", err) + } + + // Iterate through installed providers and create a list of upgrade plans. + genericProviders := []operatorv1.GenericProvider{} + + var contract string + + // Get Core Providers. + var coreProviderList operatorv1.CoreProviderList + + if err := client.List(ctx, &coreProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of core providers from the server: %w", err) + } + + for i := range coreProviderList.Items { + contract = *coreProviderList.Items[0].Status.Contract + + genericProviders = append(genericProviders, &coreProviderList.Items[i]) + } + + // Get Bootstrap Providers. + var bootstrapProviderList operatorv1.BootstrapProviderList + + if err := client.List(ctx, &bootstrapProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of bootstrap providers from the server: %w", err) + } + + for i := range bootstrapProviderList.Items { + genericProviders = append(genericProviders, &bootstrapProviderList.Items[i]) + } + + // Get Control Plane Providers. + var controlPlaneProviderList operatorv1.ControlPlaneProviderList + + if err := client.List(ctx, &controlPlaneProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of control plane providers from the server: %w", err) + } + + for i := range controlPlaneProviderList.Items { + genericProviders = append(genericProviders, &controlPlaneProviderList.Items[i]) + } + + // Get Infrastructure Providers. + var infrastructureProviderList operatorv1.InfrastructureProviderList + + if err := client.List(ctx, &infrastructureProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of infrastructure providers from the server: %w", err) + } + + for i := range infrastructureProviderList.Items { + genericProviders = append(genericProviders, &infrastructureProviderList.Items[i]) + } + + // Get Addon Providers. + var addonProviderList operatorv1.AddonProviderList + + if err := client.List(ctx, &addonProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of addon providers from the server: %w", err) + } + + for i := range addonProviderList.Items { + genericProviders = append(genericProviders, &addonProviderList.Items[i]) + } + + // Get IPAM Providers. + var ipamProviderList operatorv1.IPAMProviderList + + if err := client.List(ctx, &ipamProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of ipam providers from the server: %w", err) + } + + for i := range ipamProviderList.Items { + genericProviders = append(genericProviders, &ipamProviderList.Items[i]) + } + + // Get Runtime Extension Providers. + var runtimeExtensionProviderList operatorv1.RuntimeExtensionProviderList + + if err := client.List(ctx, &runtimeExtensionProviderList); err != nil { + return nil, "", fmt.Errorf("cannot get a list of runtime extension providers from the server: %w", err) + } + + for i := range runtimeExtensionProviderList.Items { + genericProviders = append(genericProviders, &runtimeExtensionProviderList.Items[i]) + } + + return genericProviders, contract, nil +} + +func getProviderFetchConfig(ctx context.Context, genericProvider operatorv1.GenericProvider) (providerSource, providerSourceType, error) { + // Check that fetch url was provider by user. + spec := genericProvider.GetSpec() + if spec.FetchConfig != nil && spec.FetchConfig.URL != "" { + return providerSource(spec.FetchConfig.URL), providerSourceTypeCustomURL, nil + } + + // Get fetch url from clusterctl configuration. + // TODO: support custom clusterctl configuration. + configClient, err := configclient.New(ctx, "") + if err != nil { + return "", "", err + } + + providerConfig, err := configClient.Providers().Get(genericProvider.GetName(), util.ClusterctlProviderType(genericProvider)) + if err != nil { + // TODO: implement support of fetching data from config maps + // This is a temporary fix for providers installed from config maps + if strings.Contains(err.Error(), "failed to get configuration") { + return "", providerSourceTypeConfigMap, nil + } + + return "", "", err + } + + return providerSource(providerConfig.URL()), providerSourceTypeBuiltin, nil } diff --git a/cmd/plugin/cmd/utils.go b/cmd/plugin/cmd/utils.go index 1322afaa9..936f34256 100644 --- a/cmd/plugin/cmd/utils.go +++ b/cmd/plugin/cmd/utils.go @@ -42,6 +42,12 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) +const ( + // We have to specify a version here, because if we set "latest", clusterctl libs will try to fetch metadata.yaml file for the latest + // release and fail since CAPI operator doesn't provide this file. + capiOperatorManifestsURL = "https://github.com/kubernetes-sigs/cluster-api-operator/releases/v0.1.0/operator-components.yaml" +) + var capiOperatorLabels = map[string]string{ "clusterctl.cluster.x-k8s.io/core": "capi-operator", "control-plane": "controller-manager", From f02776a99f370a2be2a0d23789b37e1929b0112f Mon Sep 17 00:00:00 2001 From: Mikhail Fedosin Date: Thu, 8 Feb 2024 17:21:14 +0100 Subject: [PATCH 2/3] test: add "upgrade plan" tests --- cmd/plugin/cmd/upgrade_plan.go | 38 +++--- cmd/plugin/cmd/upgrade_plan_test.go | 179 ++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 cmd/plugin/cmd/upgrade_plan_test.go diff --git a/cmd/plugin/cmd/upgrade_plan.go b/cmd/plugin/cmd/upgrade_plan.go index 5f0ab0b6e..ec0b3eac9 100644 --- a/cmd/plugin/cmd/upgrade_plan.go +++ b/cmd/plugin/cmd/upgrade_plan.go @@ -31,6 +31,7 @@ import ( clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" "sigs.k8s.io/cluster-api-operator/util" @@ -123,6 +124,11 @@ func runUpgradePlan() error { upgradePlanOpts.kubeconfig = GetKubeconfigLocation() } + client, err := CreateKubeClient(upgradePlanOpts.kubeconfig, upgradePlanOpts.kubeconfigContext) + if err != nil { + return fmt.Errorf("cannot create a client: %w", err) + } + certManUpgradePlan, err := planCertManagerUpgrade(ctx, upgradePlanOpts) if err != nil { return err @@ -138,7 +144,7 @@ func runUpgradePlan() error { log.Info("There are no managed Cert-Manager installations found") } - capiOperatorUpgradePlan, err := planCAPIOperatorUpgrade(ctx, upgradePlanOpts) + capiOperatorUpgradePlan, err := planCAPIOperatorUpgrade(ctx, client) if err != nil { return err } @@ -153,7 +159,7 @@ func runUpgradePlan() error { log.Info("CAPI operator is not managed by the plugin and won't be modified during upgrade") } - upgradePlan, err := planUpgrade(ctx) + upgradePlan, err := planUpgrade(ctx, client) if err != nil { return err } @@ -233,16 +239,11 @@ func planCertManagerUpgrade(ctx context.Context, opts *upgradePlanOptions) (cert }, nil } -func planCAPIOperatorUpgrade(ctx context.Context, opts *upgradePlanOptions) (capiOperatorUpgradePlan, error) { +func planCAPIOperatorUpgrade(ctx context.Context, client ctrlclient.Client) (capiOperatorUpgradePlan, error) { upgradePlan := capiOperatorUpgradePlan{} log.Info("Checking CAPI Operator version...") - client, err := CreateKubeClient(opts.kubeconfig, opts.kubeconfigContext) - if err != nil { - return upgradePlan, fmt.Errorf("cannot create a client: %w", err) - } - capiOperatorDeployment, err := GetDeploymentByLabels(ctx, client, capiOperatorLabels) if err != nil { return upgradePlan, fmt.Errorf("cannot get CAPI operator deployment: %w", err) @@ -294,12 +295,8 @@ func isCAPIOperatorExternallyManaged(deployment *appsv1.Deployment) bool { return deployment.Labels[clusterv1.ProviderNameLabel] != capiOperatorProviderName } -func planUpgrade(ctx context.Context) (upgradePlan, error) { - if initOpts.kubeconfig == "" { - initOpts.kubeconfig = GetKubeconfigLocation() - } - - genericProviders, contract, err := getInstalledProviders(ctx) +func planUpgrade(ctx context.Context, client ctrlclient.Client) (upgradePlan, error) { + genericProviders, contract, err := getInstalledProviders(ctx, client) if err != nil { return upgradePlan{}, fmt.Errorf("cannot get installed providers: %w", err) } @@ -343,16 +340,11 @@ func planUpgrade(ctx context.Context) (upgradePlan, error) { return upgradePlan{Contract: contract, Providers: upgradeItems}, nil } -func getInstalledProviders(ctx context.Context) ([]operatorv1.GenericProvider, string, error) { - client, err := CreateKubeClient(initOpts.kubeconfig, initOpts.kubeconfigContext) - if err != nil { - return nil, "", fmt.Errorf("cannot create a client: %w", err) - } - +func getInstalledProviders(ctx context.Context, client ctrlclient.Client) ([]operatorv1.GenericProvider, string, error) { // Iterate through installed providers and create a list of upgrade plans. genericProviders := []operatorv1.GenericProvider{} - var contract string + contract := "v1beta1" // Get Core Providers. var coreProviderList operatorv1.CoreProviderList @@ -361,9 +353,11 @@ func getInstalledProviders(ctx context.Context) ([]operatorv1.GenericProvider, s return nil, "", fmt.Errorf("cannot get a list of core providers from the server: %w", err) } - for i := range coreProviderList.Items { + if len(coreProviderList.Items) == 1 && coreProviderList.Items[0].Status.Contract != nil { contract = *coreProviderList.Items[0].Status.Contract + } + for i := range coreProviderList.Items { genericProviders = append(genericProviders, &coreProviderList.Items[i]) } diff --git a/cmd/plugin/cmd/upgrade_plan_test.go b/cmd/plugin/cmd/upgrade_plan_test.go new file mode 100644 index 000000000..a3920371d --- /dev/null +++ b/cmd/plugin/cmd/upgrade_plan_test.go @@ -0,0 +1,179 @@ +/* +Copyright 2024 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 ( + "testing" + + . "github.com/onsi/gomega" + clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" + "sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider" + "sigs.k8s.io/cluster-api-operator/util" +) + +func TestUpgradePlan(t *testing.T) { + tests := []struct { + name string + opts *initOptions + customURL string + wantedUpgradePlan upgradePlan + wantedProviders []genericprovider.GenericProvider + wantErr bool + }{ + { + name: "no providers", + wantedUpgradePlan: upgradePlan{ + Contract: "v1beta1", + Providers: []upgradeItem{}, + }, + wantErr: false, + opts: &initOptions{}, + }, + { + name: "builtin core provider", + wantedUpgradePlan: upgradePlan{ + Contract: "v1beta1", + Providers: []upgradeItem{ + { + Name: "cluster-api", + Namespace: "capi-system", + Type: "core", + CurrentVersion: "v1.6.0", + Source: "https://github.com/kubernetes-sigs/cluster-api/releases/latest/core-components.yaml", + SourceType: providerSourceTypeBuiltin, + }, + }, + }, + wantedProviders: []genericprovider.GenericProvider{ + generateGenericProvider(clusterctlv1.CoreProviderType, "cluster-api", "capi-system", "v1.6.0", "", ""), + }, + wantErr: false, + opts: &initOptions{ + coreProvider: "cluster-api:capi-system:v1.6.0", + targetNamespace: "capi-operator-system", + }, + }, + { + name: "custom infra provider", + customURL: "https://github.com/kubernetes-sigs/cluster-api/releases/latest/core-components.yaml", + wantedUpgradePlan: upgradePlan{ + Contract: "v1beta1", + Providers: []upgradeItem{ + { + Name: "docker", + Namespace: "capi-system", + Type: "infrastructure", + CurrentVersion: "v1.6.0", + Source: "https://github.com/kubernetes-sigs/cluster-api/releases/latest/core-components.yaml", + SourceType: providerSourceTypeCustomURL, + }, + }, + }, + wantedProviders: []genericprovider.GenericProvider{ + generateGenericProvider(clusterctlv1.InfrastructureProviderType, "docker", "capi-system", "v1.6.0", "", ""), + }, + wantErr: false, + opts: &initOptions{ + infrastructureProviders: []string{"docker:capi-system:v1.6.0"}, + targetNamespace: "capi-operator-system", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + resources := []ctrlclient.Object{} + + for _, provider := range tt.wantedProviders { + resources = append(resources, provider) + } + + err := initProviders(ctx, env, tt.opts) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + + return + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + + for _, genericProvider := range tt.wantedProviders { + g.Eventually(func() (bool, error) { + provider, err := getGenericProvider(ctx, env, string(util.ClusterctlProviderType(genericProvider)), genericProvider.GetName(), genericProvider.GetNamespace()) + if err != nil { + return false, err + } + + if provider.GetSpec().Version != genericProvider.GetSpec().Version { + return false, nil + } + + return true, nil + }, waitShort).Should(BeTrue()) + } + + // Init doesn't support custom URLs yet, so we have to update providers here + if tt.customURL != "" { + for _, genericProvider := range tt.wantedProviders { + provider, err := getGenericProvider(ctx, env, string(util.ClusterctlProviderType(genericProvider)), genericProvider.GetName(), genericProvider.GetNamespace()) + g.Expect(err).NotTo(HaveOccurred()) + + spec := provider.GetSpec() + spec.FetchConfig = &operatorv1.FetchConfiguration{ + URL: tt.customURL, + } + + provider.SetSpec(spec) + + g.Expect(env.Update(ctx, provider)).NotTo(HaveOccurred()) + + g.Eventually(func() (bool, error) { + provider, err := getGenericProvider(ctx, env, string(util.ClusterctlProviderType(genericProvider)), genericProvider.GetName(), genericProvider.GetNamespace()) + if err != nil { + return false, err + } + + if provider.GetSpec().FetchConfig == nil || provider.GetSpec().FetchConfig.URL != tt.customURL { + return false, nil + } + + return true, nil + }, waitShort).Should(BeTrue()) + } + } + + // Run upgrade plan + upgradePlan, err := planUpgrade(ctx, env) + g.Expect(err).NotTo(HaveOccurred()) + + for i, provider := range upgradePlan.Providers { + g.Expect(provider.Name).To(Equal(tt.wantedUpgradePlan.Providers[i].Name)) + g.Expect(provider.Namespace).To(Equal(tt.wantedUpgradePlan.Providers[i].Namespace)) + g.Expect(provider.Type).To(Equal(tt.wantedUpgradePlan.Providers[i].Type)) + g.Expect(provider.CurrentVersion).To(Equal(tt.wantedUpgradePlan.Providers[i].CurrentVersion)) + g.Expect(provider.Source).To(Equal(tt.wantedUpgradePlan.Providers[i].Source)) + g.Expect(provider.SourceType).To(Equal(tt.wantedUpgradePlan.Providers[i].SourceType)) + } + + g.Expect(env.CleanupAndWait(ctx, resources...)).To(Succeed()) + }) + } +} From 7e6e7ebb8fa87f939fd27a88d554b9cb1324c7e7 Mon Sep 17 00:00:00 2001 From: Mikhail Fedosin Date: Thu, 8 Feb 2024 20:14:24 +0100 Subject: [PATCH 3/3] fix: remove TestDeployCAPIOperator This test overwrites exiting CRDs with older versions, which breaks futher tests. --- cmd/plugin/cmd/init_test.go | 97 ------------------------------------- 1 file changed, 97 deletions(-) diff --git a/cmd/plugin/cmd/init_test.go b/cmd/plugin/cmd/init_test.go index c9e60be85..94665fac0 100644 --- a/cmd/plugin/cmd/init_test.go +++ b/cmd/plugin/cmd/init_test.go @@ -19,7 +19,6 @@ package cmd import ( "context" "fmt" - "os" "testing" . "github.com/onsi/gomega" @@ -29,9 +28,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" - "sigs.k8s.io/cluster-api/util/kubeconfig" operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" "sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider" @@ -374,100 +371,6 @@ func generateCAPIOperatorDeployment(name, namespace string) *appsv1.Deployment { } } -func TestDeployCAPIOperator(t *testing.T) { - g := NewWithT(t) - - envCluster := &clusterv1.Cluster{} - envCluster.Name = "test-cluster" - - kubeconfigRaw := kubeconfig.FromEnvTestConfig(env.GetConfig(), envCluster) - - tempDir := os.TempDir() - - kubeconfigFile, err := os.CreateTemp(tempDir, "kubeconfig") - g.Expect(err).NotTo(HaveOccurred()) - - defer func() { - if err := os.Remove(kubeconfigFile.Name()); err != nil { - t.Error(err) - } - }() - - _, err = kubeconfigFile.Write(kubeconfigRaw) - g.Expect(err).NotTo(HaveOccurred()) - - tests := []struct { - name string - opts *initOptions - wantedVersion string - wantErr bool - }{ - { - name: "with version", - wantedVersion: "v0.7.0", - wantErr: false, - opts: &initOptions{ - kubeconfig: kubeconfigFile.Name(), - kubeconfigContext: "@test-cluster", - operatorVersion: "v0.7.0", - }, - }, - { - name: "incorrect version", - wantErr: true, - opts: &initOptions{ - kubeconfig: kubeconfigFile.Name(), - kubeconfigContext: "@test-cluster", - operatorVersion: "v1000000", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - ctx, cancel := context.WithTimeout(context.Background(), waitLong) - - defer cancel() - - resources := []ctrlclient.Object{} - - deployment := generateCAPIOperatorDeployment("capi-operator-controller-manager", "capi-operator-system") - - err := deployCAPIOperator(ctx, tt.opts) - - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - - return - } else { - g.Expect(err).NotTo(HaveOccurred()) - } - - resources = append(resources, deployment) - - g.Eventually(func() (bool, error) { - err := env.Get(ctx, ctrlclient.ObjectKeyFromObject(deployment), deployment) - if err != nil { - return false, err - } - - return deployment != nil, nil - }, waitShort).Should(BeTrue()) - - g.Expect(deployment.Spec.Template.Spec.Containers).NotTo(BeEmpty()) - - if tt.wantedVersion != "" { - g.Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(HaveSuffix(tt.wantedVersion)) - } else { - g.Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(HaveSuffix(tt.opts.operatorVersion)) - } - - g.Expect(env.CleanupAndWait(ctx, resources...)).To(Succeed()) - }) - } -} - func generateGenericProvider(providerType clusterctlv1.ProviderType, name, namespace, version, configSecretName, configSecretNamespace string) genericprovider.GenericProvider { genericProvider := NewGenericProvider(providerType)