From ff0507e35326a2f57a5e5e90d183a98bdcb2611a Mon Sep 17 00:00:00 2001 From: Richard Draycott Date: Mon, 20 May 2024 17:20:22 +0200 Subject: [PATCH 1/2] :sparkles: Change clusterctl alpha rollout to support any controlplane that implements rolloutAfter in it's spec Signed-off-by: Richard Draycott --- cmd/clusterctl/client/alpha/controlplane.go | 210 ++++++++++++++++++ .../client/alpha/kubeadmcontrolplane.go | 75 ------- cmd/clusterctl/client/alpha/rollout.go | 2 + cmd/clusterctl/client/alpha/rollout_pauser.go | 23 +- .../client/alpha/rollout_pauser_test.go | 6 +- .../client/alpha/rollout_restarter.go | 33 ++- .../client/alpha/rollout_resumer.go | 21 +- cmd/clusterctl/cmd/rollout/restart.go | 1 + cmd/clusterctl/internal/util/obj_refs.go | 37 ++- .../src/clusterctl/commands/alpha-rollout.md | 11 + 10 files changed, 296 insertions(+), 123 deletions(-) create mode 100644 cmd/clusterctl/client/alpha/controlplane.go delete mode 100644 cmd/clusterctl/client/alpha/kubeadmcontrolplane.go diff --git a/cmd/clusterctl/client/alpha/controlplane.go b/cmd/clusterctl/client/alpha/controlplane.go new file mode 100644 index 000000000000..e14663c4c520 --- /dev/null +++ b/cmd/clusterctl/client/alpha/controlplane.go @@ -0,0 +1,210 @@ +/* +Copyright 2022 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 alpha + +import ( + "context" + "fmt" + "strings" + "time" + + openapi_v2 "github.com/google/gnostic-models/openapiv2" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" +) + +func getUnstructuredControlPlane(ctx context.Context, proxy cluster.Proxy, ref corev1.ObjectReference) (*unstructured.Unstructured, error) { + c, err := proxy.NewClient(ctx) + if err != nil { + return nil, err + } + if ref.APIVersion == "" { + ref.APIVersion = DefaultAPIVersion + } + gvk := schema.GroupVersionKind{ + Group: "controlplane.cluster.x-k8s.io", + Version: ref.APIVersion, + Kind: ref.Kind, + } + + // Create an unstructured object + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + obj.SetNamespace(ref.Namespace) + obj.SetName(ref.Name) + + // Fetch the resource + err = c.Get(ctx, client.ObjectKey{ + Namespace: ref.Namespace, + Name: ref.Name, + }, obj) + + if err != nil { + return nil, fmt.Errorf("failed to get unstructured object: %v", err) + } + + return obj, nil +} + +// checkResourceConditions checks for specific conditions on the fetched resource. +func checkControlPlaneRolloutAfter(obj *unstructured.Unstructured) error { + rolloutAfter, _, err := unstructured.NestedString(obj.Object, "spec", "rolloutAfter") + + if err != nil { + return errors.Wrapf(err, "error accessing rolloutAfter in spec: %s/%s", + obj.GetNamespace(), obj.GetName()) + } + if rolloutAfter == "" { + return nil + } + rolloutTime, err := time.Parse(time.RFC3339, rolloutAfter) + if err != nil { + return errors.Wrapf(err, "invalid rolloutAfter format: %s/%s", + obj.GetNamespace(), obj.GetName()) + } + if rolloutTime.After(time.Now()) { + return errors.Errorf("can't update KubeadmControlPlane (remove 'spec.rolloutAfter' first): %v/%v", obj.GetKind(), obj.GetName()) + } + + return nil +} + +// setRolloutAfterOnControlPlane sets the rolloutAfter field on a generic resource. +func setRolloutAfterOnControlPlane(ctx context.Context, proxy cluster.Proxy, ref corev1.ObjectReference) error { + patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(`{"spec":{"rolloutAfter":"%v"}}`, time.Now().Format(time.RFC3339)))) + return patchControlPlane(ctx, proxy, ref, patch) +} + +// patchControlPlane applies a patch to an unstructured controlplane. +func patchControlPlane(ctx context.Context, proxy cluster.Proxy, ref corev1.ObjectReference, patch client.Patch) error { + c, err := proxy.NewClient(ctx) + if err != nil { + return err + } + if ref.APIVersion == "" { + ref.APIVersion = DefaultAPIVersion + } + gvk := schema.GroupVersionKind{ + Group: "controlplane.cluster.x-k8s.io", + Version: ref.APIVersion, + Kind: ref.Kind, + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + objKey := client.ObjectKey{ + Namespace: ref.Namespace, + Name: ref.Name, + } + + // Get the current state of the resource + if err := c.Get(ctx, objKey, obj); err != nil { + return errors.Wrapf(err, "failed to get ControlPlane %s/%s", + obj.GetKind(), obj.GetName()) + } + + // Apply the patch + if err := c.Patch(ctx, obj, patch); err != nil { + return errors.Wrapf(err, "failed while patching ControlPlane %s/%s", + obj.GetKind(), obj.GetName()) + } + + return nil +} + +func resourceHasRolloutAfter(proxy cluster.Proxy, ref corev1.ObjectReference) (bool, error) { + if ref.APIVersion == "" { + ref.APIVersion = DefaultAPIVersion + } + + config, err := proxy.GetConfig() + if err != nil { + return false, err + } + + if config == nil { + return false, nil + } + + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return false, err + } + + //Fetch the OpenAPI schema + openAPISchema, err := discoveryClient.OpenAPISchema() + if err != nil { + return false, err + } + + // Iterate over the schema definitions to find the resource + if openAPISchema == nil { + return false, fmt.Errorf("openAPI schema is nil") + } + + for _, definition := range openAPISchema.GetDefinitions().AdditionalProperties { + // Ensure the definition value is not nil + if definition == nil || definition.Value == nil { + continue + } + + // Check if the definition matches the resource we are looking for + resourceDefName := fmt.Sprintf("%s.%s.%s", "io.x-k8s.cluster.controlplane", ref.APIVersion, ref.Kind) + if findSpecPropertyForResource(definition, resourceDefName, "rolloutAfter") { + return true, nil + } + + } + + return false, fmt.Errorf("resource definition for %s.%s.%s not found", "io.x-k8s.cluster.controlplane", ref.APIVersion, ref.Kind) +} + +func findSpecPropertyForResource(resourceDefinition *openapi_v2.NamedSchema, resourceDefName, field string) bool { + if !strings.HasSuffix(strings.ToLower(resourceDefinition.Name), resourceDefName) { + return false + } + + // Find spec field in crd properties + properties := resourceDefinition.Value.GetProperties().AdditionalProperties + if properties == nil { + return false + } + + for _, property := range properties { + if property.GetName() == "spec" { + // Check if rolloutAfter exists in spec properties + specProperties := property.GetValue().GetProperties().AdditionalProperties + if specProperties == nil { + return false + } + for _, specProperty := range specProperties { + if specProperty.GetName() == field { + return true + } + } + } + } + + return false +} diff --git a/cmd/clusterctl/client/alpha/kubeadmcontrolplane.go b/cmd/clusterctl/client/alpha/kubeadmcontrolplane.go deleted file mode 100644 index 0d8c80e858a7..000000000000 --- a/cmd/clusterctl/client/alpha/kubeadmcontrolplane.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2022 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 alpha - -import ( - "context" - "fmt" - "time" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" -) - -// getKubeadmControlPlane retrieves the KubeadmControlPlane object corresponding to the name and namespace specified. -func getKubeadmControlPlane(ctx context.Context, proxy cluster.Proxy, name, namespace string) (*controlplanev1.KubeadmControlPlane, error) { - kcpObj := &controlplanev1.KubeadmControlPlane{} - c, err := proxy.NewClient(ctx) - if err != nil { - return nil, err - } - kcpObjKey := client.ObjectKey{ - Namespace: namespace, - Name: name, - } - if err := c.Get(ctx, kcpObjKey, kcpObj); err != nil { - return nil, errors.Wrapf(err, "failed to get KubeadmControlPlane %s/%s", - kcpObjKey.Namespace, kcpObjKey.Name) - } - return kcpObj, nil -} - -// setRolloutAfterOnKCP sets KubeadmControlPlane.spec.rolloutAfter. -func setRolloutAfterOnKCP(ctx context.Context, proxy cluster.Proxy, name, namespace string) error { - patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(`{"spec":{"rolloutAfter":"%v"}}`, time.Now().Format(time.RFC3339)))) - return patchKubeadmControlPlane(ctx, proxy, name, namespace, patch) -} - -// patchKubeadmControlPlane applies a patch to a KubeadmControlPlane. -func patchKubeadmControlPlane(ctx context.Context, proxy cluster.Proxy, name, namespace string, patch client.Patch) error { - cFrom, err := proxy.NewClient(ctx) - if err != nil { - return err - } - kcpObj := &controlplanev1.KubeadmControlPlane{} - kcpObjKey := client.ObjectKey{ - Namespace: namespace, - Name: name, - } - if err := cFrom.Get(ctx, kcpObjKey, kcpObj); err != nil { - return errors.Wrapf(err, "failed to get KubeadmControlPlane %s/%s", kcpObj.GetNamespace(), kcpObj.GetName()) - } - - if err := cFrom.Patch(ctx, kcpObj, patch); err != nil { - return errors.Wrapf(err, "failed while patching KubeadmControlPlane %s/%s", kcpObj.GetNamespace(), kcpObj.GetName()) - } - return nil -} diff --git a/cmd/clusterctl/client/alpha/rollout.go b/cmd/clusterctl/client/alpha/rollout.go index 8736ae79df0d..8e718ebdacec 100644 --- a/cmd/clusterctl/client/alpha/rollout.go +++ b/cmd/clusterctl/client/alpha/rollout.go @@ -29,6 +29,8 @@ const ( MachineDeployment = "machinedeployment" // KubeadmControlPlane is a resource type. KubeadmControlPlane = "kubeadmcontrolplane" + // DefaultAPIVersion is what clusterctl will assume if none is provided + DefaultAPIVersion = "v1beta1" ) var validResourceTypes = []string{ diff --git a/cmd/clusterctl/client/alpha/rollout_pauser.go b/cmd/clusterctl/client/alpha/rollout_pauser.go index 582c2cb7409d..19e4337db259 100644 --- a/cmd/clusterctl/client/alpha/rollout_pauser.go +++ b/cmd/clusterctl/client/alpha/rollout_pauser.go @@ -27,7 +27,6 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" - "sigs.k8s.io/cluster-api/util/annotations" ) // ObjectPauser will issue a pause on the specified cluster-api resource. @@ -44,19 +43,19 @@ func (r *rollout) ObjectPauser(ctx context.Context, proxy cluster.Proxy, ref cor if err := pauseMachineDeployment(ctx, proxy, ref.Name, ref.Namespace); err != nil { return err } - case KubeadmControlPlane: - kcp, err := getKubeadmControlPlane(ctx, proxy, ref.Name, ref.Namespace) - if err != nil || kcp == nil { + default: + obj, err := getUnstructuredControlPlane(ctx, proxy, ref) + if err != nil || obj == nil { return errors.Wrapf(err, "failed to fetch %v/%v", ref.Kind, ref.Name) } - if annotations.HasPaused(kcp.GetObjectMeta()) { - return errors.Errorf("KubeadmControlPlane is already paused: %v/%v\n", ref.Kind, ref.Name) //nolint:revive // KubeadmControlPlane is intentionally capitalized. + + annotations := obj.GetAnnotations() + if paused, ok := annotations["cluster.x-k8s.io/paused"]; ok && paused == "true" { + return errors.Errorf("can't perform operations on paused resource (remove annotation 'cluster.x-k8s.io/paused' first): %v/%v", obj.GetKind(), obj.GetName()) } - if err := pauseKubeadmControlPlane(ctx, proxy, ref.Name, ref.Namespace); err != nil { + if err := pauseControlPlane(ctx, proxy, ref); err != nil { return err } - default: - return errors.Errorf("Invalid resource type %q, valid values are %v", ref.Kind, validResourceTypes) } return nil } @@ -67,8 +66,8 @@ func pauseMachineDeployment(ctx context.Context, proxy cluster.Proxy, name, name return patchMachineDeployment(ctx, proxy, name, namespace, patch) } -// pauseKubeadmControlPlane sets paused annotation to true. -func pauseKubeadmControlPlane(ctx context.Context, proxy cluster.Proxy, name, namespace string) error { +// pauseControlPlane sets paused annotation to true. +func pauseControlPlane(ctx context.Context, proxy cluster.Proxy, ref corev1.ObjectReference) error { patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf("{\"metadata\":{\"annotations\":{%q: \"%t\"}}}", clusterv1.PausedAnnotation, true))) - return patchKubeadmControlPlane(ctx, proxy, name, namespace, patch) + return patchControlPlane(ctx, proxy, ref, patch) } diff --git a/cmd/clusterctl/client/alpha/rollout_pauser_test.go b/cmd/clusterctl/client/alpha/rollout_pauser_test.go index ae5d0b939e6e..ad25e82a2257 100644 --- a/cmd/clusterctl/client/alpha/rollout_pauser_test.go +++ b/cmd/clusterctl/client/alpha/rollout_pauser_test.go @@ -97,7 +97,8 @@ func Test_ObjectPauser(t *testing.T) { objs: []client.Object{ &controlplanev1.KubeadmControlPlane{ TypeMeta: metav1.TypeMeta{ - Kind: "KubeadmControlPlane", + Kind: "KubeadmControlPlane", + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Namespace: "default", @@ -120,7 +121,8 @@ func Test_ObjectPauser(t *testing.T) { objs: []client.Object{ &controlplanev1.KubeadmControlPlane{ TypeMeta: metav1.TypeMeta{ - Kind: "KubeadmControlPlane", + Kind: "KubeadmControlPlane", + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Namespace: "default", diff --git a/cmd/clusterctl/client/alpha/rollout_restarter.go b/cmd/clusterctl/client/alpha/rollout_restarter.go index d16392d5591a..25b6e42a9a7e 100644 --- a/cmd/clusterctl/client/alpha/rollout_restarter.go +++ b/cmd/clusterctl/client/alpha/rollout_restarter.go @@ -24,7 +24,6 @@ import ( corev1 "k8s.io/api/core/v1" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" - "sigs.k8s.io/cluster-api/util/annotations" ) // ObjectRestarter will issue a restart on the specified cluster-api resource. @@ -33,7 +32,7 @@ func (r *rollout) ObjectRestarter(ctx context.Context, proxy cluster.Proxy, ref case MachineDeployment: deployment, err := getMachineDeployment(ctx, proxy, ref.Name, ref.Namespace) if err != nil || deployment == nil { - return errors.Wrapf(err, "failed to fetch %v/%v", ref.Kind, ref.Name) + return errors.Wrapf(err, "failed to fetch resource %v/%v", ref.Kind, ref.Name) } if deployment.Spec.Paused { return errors.Errorf("can't restart paused MachineDeployment (run rollout resume first): %v/%v", ref.Kind, ref.Name) @@ -44,22 +43,32 @@ func (r *rollout) ObjectRestarter(ctx context.Context, proxy cluster.Proxy, ref if err := setRolloutAfterOnMachineDeployment(ctx, proxy, ref.Name, ref.Namespace); err != nil { return err } - case KubeadmControlPlane: - kcp, err := getKubeadmControlPlane(ctx, proxy, ref.Name, ref.Namespace) - if err != nil || kcp == nil { + default: + _, err := resourceHasRolloutAfter(proxy, ref) + if err != nil { + return errors.Errorf("Invalid resource type %v. Resource must implement rolloutAfter in it's spec", ref.Kind) + } + obj, err := getUnstructuredControlPlane(ctx, proxy, ref) + if err != nil || obj == nil { return errors.Wrapf(err, "failed to fetch %v/%v", ref.Kind, ref.Name) } - if annotations.HasPaused(kcp.GetObjectMeta()) { - return errors.Errorf("can't restart paused KubeadmControlPlane (remove annotation 'cluster.x-k8s.io/paused' first): %v/%v", ref.Kind, ref.Name) + + annotations := obj.GetAnnotations() + if paused, ok := annotations["cluster.x-k8s.io/paused"]; ok && paused == "true" { + return errors.Errorf("can't perform operations on paused resource (remove annotation 'cluster.x-k8s.io/paused' first): %v/%v", obj.GetKind(), obj.GetName()) } - if kcp.Spec.RolloutAfter != nil && kcp.Spec.RolloutAfter.After(time.Now()) { - return errors.Errorf("can't update KubeadmControlPlane (remove 'spec.rolloutAfter' first): %v/%v", ref.Kind, ref.Name) + + if err := checkControlPlaneRolloutAfter(obj); err != nil { + // if _, ok := err.(*errRolloutAfterNotFound); ok { + // return errors.Errorf("Invalid resource type %v. Resource must implement rolloutAfter in it's spec", ref.Kind) + // } + return errors.Errorf("err: %s, can't update ControlPlane (remove 'spec.rolloutAfter' first): %v/%v", err.Error(), ref.Kind, ref.Name) } - if err := setRolloutAfterOnKCP(ctx, proxy, ref.Name, ref.Namespace); err != nil { + + if err := setRolloutAfterOnControlPlane(ctx, proxy, ref); err != nil { return err } - default: - return errors.Errorf("Invalid resource type %v. Valid values: %v", ref.Kind, validResourceTypes) + } return nil } diff --git a/cmd/clusterctl/client/alpha/rollout_resumer.go b/cmd/clusterctl/client/alpha/rollout_resumer.go index b224e91e87a0..7d064d455633 100644 --- a/cmd/clusterctl/client/alpha/rollout_resumer.go +++ b/cmd/clusterctl/client/alpha/rollout_resumer.go @@ -28,7 +28,6 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" - "sigs.k8s.io/cluster-api/util/annotations" ) // ObjectResumer will issue a resume on the specified cluster-api resource. @@ -45,19 +44,19 @@ func (r *rollout) ObjectResumer(ctx context.Context, proxy cluster.Proxy, ref co if err := resumeMachineDeployment(ctx, proxy, ref.Name, ref.Namespace); err != nil { return err } - case KubeadmControlPlane: - kcp, err := getKubeadmControlPlane(ctx, proxy, ref.Name, ref.Namespace) - if err != nil || kcp == nil { + default: + obj, err := getUnstructuredControlPlane(ctx, proxy, ref) + if err != nil || obj == nil { return errors.Wrapf(err, "failed to fetch %v/%v", ref.Kind, ref.Name) } - if !annotations.HasPaused(kcp.GetObjectMeta()) { - return errors.Errorf("KubeadmControlPlane is not currently paused: %v/%v\n", ref.Kind, ref.Name) //nolint:revive // KubeadmControlPlane is intentionally capitalized. + + annotations := obj.GetAnnotations() + if paused, ok := annotations["cluster.x-k8s.io/paused"]; !ok || paused == "false" { + return errors.Errorf("can't perform operations on paused resource (remove annotation 'cluster.x-k8s.io/paused' first): %v/%v", obj.GetKind(), obj.GetName()) } - if err := resumeKubeadmControlPlane(ctx, proxy, ref.Name, ref.Namespace); err != nil { + if err := resumeControlPlane(ctx, proxy, ref); err != nil { return err } - default: - return errors.Errorf("invalid resource type %q, valid values are %v", ref.Kind, validResourceTypes) } return nil } @@ -70,10 +69,10 @@ func resumeMachineDeployment(ctx context.Context, proxy cluster.Proxy, name, nam } // resumeKubeadmControlPlane removes paused annotation. -func resumeKubeadmControlPlane(ctx context.Context, proxy cluster.Proxy, name, namespace string) error { +func resumeControlPlane(ctx context.Context, proxy cluster.Proxy, ref corev1.ObjectReference) error { // In the paused annotation we must replace slashes to ~1, see https://datatracker.ietf.org/doc/html/rfc6901#section-3. pausedAnnotation := strings.Replace(clusterv1.PausedAnnotation, "/", "~1", -1) patch := client.RawPatch(types.JSONPatchType, []byte(fmt.Sprintf("[{\"op\": \"remove\", \"path\": \"/metadata/annotations/%s\"}]", pausedAnnotation))) - return patchKubeadmControlPlane(ctx, proxy, name, namespace, patch) + return patchControlPlane(ctx, proxy, ref, patch) } diff --git a/cmd/clusterctl/cmd/rollout/restart.go b/cmd/clusterctl/cmd/rollout/restart.go index b582c6c46411..7e062bb33a3b 100644 --- a/cmd/clusterctl/cmd/rollout/restart.go +++ b/cmd/clusterctl/cmd/rollout/restart.go @@ -31,6 +31,7 @@ type restartOptions struct { kubeconfigContext string resources []string namespace string + apiVersion string } var restartOpt = &restartOptions{} diff --git a/cmd/clusterctl/internal/util/obj_refs.go b/cmd/clusterctl/internal/util/obj_refs.go index 56b9c30d44e8..e14dd5d28820 100644 --- a/cmd/clusterctl/internal/util/obj_refs.go +++ b/cmd/clusterctl/internal/util/obj_refs.go @@ -24,7 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" ) -// GetObjectReferences accepts arguments in resource/name form (e.g. 'resource/') and returns a ObjectReference for each resource/name. +// GetObjectReferences accepts arguments in resource.apiversion/name or resource/name form (e.g. 'resource.apiversion/' or 'resource/') and returns an ObjectReference for each resource/name. func GetObjectReferences(namespace string, args ...string) ([]corev1.ObjectReference, error) { var objRefs []corev1.ObjectReference if ok, err := hasCombinedTypeArgs(args); ok { @@ -41,7 +41,7 @@ func GetObjectReferences(namespace string, args ...string) ([]corev1.ObjectRefer } } } else { - return objRefs, fmt.Errorf("arguments must be in resource/name format (e.g. machinedeployment/md-1)") + return objRefs, fmt.Errorf("arguments must be in resource.apiversion/name or resource/name format (e.g. deployment.v1/md-1 or deployment/md-1)") } return objRefs, nil } @@ -62,13 +62,13 @@ func hasCombinedTypeArgs(args []string) (bool, error) { baseCmdSlice := strings.Split(os.Args[0], "/") baseCmd = baseCmdSlice[len(baseCmdSlice)-1] } - return true, fmt.Errorf("there is no need to specify a resource type as a separate argument when passing arguments in resource/name form (e.g. '%s get resource/' instead of '%s get resource resource/'", baseCmd, baseCmd) + return true, fmt.Errorf("there is no need to specify a resource type as a separate argument when passing arguments in resource.apiversion/name or resource/name form (e.g. '%s get resource.apiversion/' instead of '%s get resource resource.apiversion/'", baseCmd, baseCmd) default: return false, nil } } -// convertToObjectRef handles type/name resource formats and returns a ObjectReference +// convertToObjectRef handles resource.apiversion/name or resource/name resource formats and returns an ObjectReference // (empty or not), whether it successfully found one, and an error. func convertToObjectRef(namespace, s string) (corev1.ObjectReference, bool, error) { if !strings.Contains(s, "/") { @@ -76,15 +76,30 @@ func convertToObjectRef(namespace, s string) (corev1.ObjectReference, bool, erro } seg := strings.Split(s, "/") if len(seg) != 2 { - return corev1.ObjectReference{}, false, fmt.Errorf("arguments in resource/name form may not have more than one slash") + return corev1.ObjectReference{}, false, fmt.Errorf("arguments in resource.apiversion/name or resource/name form may not have more than one slash") } - resource, name := seg[0], seg[1] - if resource == "" || name == "" { - return corev1.ObjectReference{}, false, fmt.Errorf("arguments in resource/name form must have a single resource and name") + resourceVersion, name := seg[0], seg[1] + if resourceVersion == "" || name == "" { + return corev1.ObjectReference{}, false, fmt.Errorf("arguments in resource.apiversion/name or resource/name form must have a resource and name") } + + var resource, apiVersion string + resourceVersionSeg := strings.Split(resourceVersion, ".") + if len(resourceVersionSeg) == 2 { + resource, apiVersion = resourceVersionSeg[0], resourceVersionSeg[1] + } else if len(resourceVersionSeg) == 1 { + resource = resourceVersionSeg[0] + } else { + return corev1.ObjectReference{}, false, fmt.Errorf("arguments in resource.apiversion/name or resource/name form must have a valid resource and optionally an apiversion separated by a dot") + } + if resource == "" { + return corev1.ObjectReference{}, false, fmt.Errorf("resource in resource.apiversion/name or resource/name form must not be empty") + } + return corev1.ObjectReference{ - Kind: resource, - Name: name, - Namespace: namespace, + Kind: resource, + Name: name, + Namespace: namespace, + APIVersion: apiVersion, }, true, nil } diff --git a/docs/book/src/clusterctl/commands/alpha-rollout.md b/docs/book/src/clusterctl/commands/alpha-rollout.md index d0c3cc4770e4..e94b463135cb 100644 --- a/docs/book/src/clusterctl/commands/alpha-rollout.md +++ b/docs/book/src/clusterctl/commands/alpha-rollout.md @@ -11,6 +11,11 @@ Currently, only the following Cluster API resources are supported by the rollout - kubeadmcontrolplanes - machinedeployments +Third party controlplane providers are supported as long as they: + +- Implement rolloutAfter in their spec +- Are in the controlplane.cluster.x-k8s.io group + ### Restart @@ -21,6 +26,12 @@ Use the `restart` sub-command to force an immediate rollout. Note that rollout r clusterctl alpha rollout restart machinedeployment/my-md-0 ``` +Or for a third party controlplane: + +```bash +clusterctl alpha rollout restart my-controlplane-kind.v1beta1/my-kcp +``` + ### Undo Use the `undo` sub-command to rollback to an earlier revision. For example, here the MachineDeployment `my-md-0` will be rolled back to revision number 3. If the `--to-revision` flag is omitted, the MachineDeployment will be rolled back to the revision immediately preceding the current one. If the desired revision does not exist, the undo will return an error. From 0523c8dee9ea69a6e75341bcc489936a1556f193 Mon Sep 17 00:00:00 2001 From: Richard Draycott Date: Thu, 13 Jun 2024 15:50:33 +0100 Subject: [PATCH 2/2] :sparkles: Fix vendoring and lint issues --- cmd/clusterctl/client/alpha/controlplane.go | 3 +-- cmd/clusterctl/client/alpha/rollout.go | 7 +------ cmd/clusterctl/client/alpha/rollout_restarter.go | 4 ---- cmd/clusterctl/cmd/rollout/restart.go | 1 - go.mod | 2 +- 5 files changed, 3 insertions(+), 14 deletions(-) diff --git a/cmd/clusterctl/client/alpha/controlplane.go b/cmd/clusterctl/client/alpha/controlplane.go index e14663c4c520..de688f2f9e3a 100644 --- a/cmd/clusterctl/client/alpha/controlplane.go +++ b/cmd/clusterctl/client/alpha/controlplane.go @@ -152,7 +152,7 @@ func resourceHasRolloutAfter(proxy cluster.Proxy, ref corev1.ObjectReference) (b return false, err } - //Fetch the OpenAPI schema + // Fetch the OpenAPI schema openAPISchema, err := discoveryClient.OpenAPISchema() if err != nil { return false, err @@ -174,7 +174,6 @@ func resourceHasRolloutAfter(proxy cluster.Proxy, ref corev1.ObjectReference) (b if findSpecPropertyForResource(definition, resourceDefName, "rolloutAfter") { return true, nil } - } return false, fmt.Errorf("resource definition for %s.%s.%s not found", "io.x-k8s.cluster.controlplane", ref.APIVersion, ref.Kind) diff --git a/cmd/clusterctl/client/alpha/rollout.go b/cmd/clusterctl/client/alpha/rollout.go index 8e718ebdacec..4fb66593f36f 100644 --- a/cmd/clusterctl/client/alpha/rollout.go +++ b/cmd/clusterctl/client/alpha/rollout.go @@ -29,15 +29,10 @@ const ( MachineDeployment = "machinedeployment" // KubeadmControlPlane is a resource type. KubeadmControlPlane = "kubeadmcontrolplane" - // DefaultAPIVersion is what clusterctl will assume if none is provided + // DefaultAPIVersion is what clusterctl will assume if none is provided. DefaultAPIVersion = "v1beta1" ) -var validResourceTypes = []string{ - MachineDeployment, - KubeadmControlPlane, -} - var validRollbackResourceTypes = []string{ MachineDeployment, } diff --git a/cmd/clusterctl/client/alpha/rollout_restarter.go b/cmd/clusterctl/client/alpha/rollout_restarter.go index 25b6e42a9a7e..e1eacd5e97c2 100644 --- a/cmd/clusterctl/client/alpha/rollout_restarter.go +++ b/cmd/clusterctl/client/alpha/rollout_restarter.go @@ -59,16 +59,12 @@ func (r *rollout) ObjectRestarter(ctx context.Context, proxy cluster.Proxy, ref } if err := checkControlPlaneRolloutAfter(obj); err != nil { - // if _, ok := err.(*errRolloutAfterNotFound); ok { - // return errors.Errorf("Invalid resource type %v. Resource must implement rolloutAfter in it's spec", ref.Kind) - // } return errors.Errorf("err: %s, can't update ControlPlane (remove 'spec.rolloutAfter' first): %v/%v", err.Error(), ref.Kind, ref.Name) } if err := setRolloutAfterOnControlPlane(ctx, proxy, ref); err != nil { return err } - } return nil } diff --git a/cmd/clusterctl/cmd/rollout/restart.go b/cmd/clusterctl/cmd/rollout/restart.go index 7e062bb33a3b..b582c6c46411 100644 --- a/cmd/clusterctl/cmd/rollout/restart.go +++ b/cmd/clusterctl/cmd/rollout/restart.go @@ -31,7 +31,6 @@ type restartOptions struct { kubeconfigContext string resources []string namespace string - apiVersion string } var restartOpt = &restartOptions{} diff --git a/go.mod b/go.mod index 40682af35cd9..8200d8188bd1 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/flatcar/ignition v0.36.2 github.com/go-logr/logr v1.4.1 github.com/gobuffalo/flect v1.0.2 + github.com/google/gnostic-models v0.6.8 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v53 v53.2.0 github.com/google/gofuzz v1.2.0 @@ -89,7 +90,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/cel-go v0.17.8 // indirect - github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect