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.