diff --git a/.golangci.yml b/.golangci.yml index af73849bac4d..34e600d8e257 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -231,7 +231,7 @@ linters: # should be removed as the referenced deprecated item is removed from the project. - linters: - staticcheck - text: 'SA1019: (bootstrapv1.ClusterStatus|DockerMachine.Spec.Bootstrapped|machineStatus.Bootstrapped|dockerMachine.Spec.Backend.Docker.Bootstrapped|dockerMachine.Spec.Bootstrapped|devMachine.Spec.Backend.Docker.Bootstrapped|c.TopologyPlan|clusterv1.ClusterClassVariableMetadata|clusterv1beta1.ClusterClassVariableMetadata|(variable|currentDefinition|specVar|newVariableDefinition|statusVarDefinition|out).DeprecatedV1Beta1Metadata) is deprecated' + text: 'SA1019: (bootstrapv1.ClusterStatus|DockerMachine.Spec.Bootstrapped|machineStatus.Bootstrapped|dockerMachine.Spec.Backend.Docker.Bootstrapped|dockerMachine.Spec.Bootstrapped|devMachine.Spec.Backend.Docker.Bootstrapped|clusterv1.ClusterClassVariableMetadata|clusterv1beta1.ClusterClassVariableMetadata|(variable|currentDefinition|specVar|newVariableDefinition|statusVarDefinition|out).DeprecatedV1Beta1Metadata) is deprecated' # Deprecations for MHC MaxUnhealthy, UnhealthyRange - linters: - staticcheck diff --git a/cmd/clusterctl/client/client.go b/cmd/clusterctl/client/client.go index 3c07bdc17a80..9c6c1b500d82 100644 --- a/cmd/clusterctl/client/client.go +++ b/cmd/clusterctl/client/client.go @@ -87,10 +87,6 @@ type AlphaClient interface { RolloutPause(ctx context.Context, options RolloutPauseOptions) error // RolloutResume provides rollout resume of paused cluster-api resources RolloutResume(ctx context.Context, options RolloutResumeOptions) error - // TopologyPlan dry runs the topology reconciler - // - // Deprecated: TopologyPlan is deprecated and will be removed in one of the upcoming releases. - TopologyPlan(ctx context.Context, options TopologyPlanOptions) (*TopologyPlanOutput, error) } // YamlPrinter exposes methods that prints the processed template and diff --git a/cmd/clusterctl/client/client_test.go b/cmd/clusterctl/client/client_test.go index b38d6fed395a..671bb9663c89 100644 --- a/cmd/clusterctl/client/client_test.go +++ b/cmd/clusterctl/client/client_test.go @@ -145,10 +145,6 @@ func (f fakeClient) RolloutResume(ctx context.Context, options RolloutResumeOpti return f.internalClient.RolloutResume(ctx, options) } -func (f fakeClient) TopologyPlan(ctx context.Context, options TopologyPlanOptions) (*cluster.TopologyPlanOutput, error) { - return f.internalClient.TopologyPlan(ctx, options) -} - // newFakeClient returns a clusterctl client that allows to execute tests on a set of fake config, fake repositories and fake clusters. // you can use WithCluster and WithRepository to prepare for the test case. func newFakeClient(ctx context.Context, configClient config.Client) *fakeClient { @@ -322,10 +318,6 @@ func (f *fakeClusterClient) WorkloadCluster() cluster.WorkloadCluster { return f.internalclient.WorkloadCluster() } -func (f *fakeClusterClient) Topology() cluster.TopologyClient { - return f.internalclient.Topology() -} - func (f *fakeClusterClient) WithObjs(objs ...client.Object) *fakeClusterClient { f.fakeProxy.WithObjs(objs...) return f diff --git a/cmd/clusterctl/client/cluster/client.go b/cmd/clusterctl/client/cluster/client.go index cb72a18fc2fa..59cd78430c88 100644 --- a/cmd/clusterctl/client/cluster/client.go +++ b/cmd/clusterctl/client/cluster/client.go @@ -81,9 +81,6 @@ type Client interface { // WorkloadCluster has methods for fetching kubeconfig of workload cluster from management cluster. WorkloadCluster() WorkloadCluster - - // Topology returns a TopologyClient that can be used for performing dry run executions of the topology reconciler. - Topology() TopologyClient } // PollImmediateWaiter tries a condition func until it returns true, an error, or the timeout is reached. @@ -147,10 +144,6 @@ func (c *clusterClient) WorkloadCluster() WorkloadCluster { return newWorkloadCluster(c.proxy) } -func (c *clusterClient) Topology() TopologyClient { - return newTopologyClient(c.proxy, c.ProviderInventory()) -} - // Option is a configuration option supplied to New. type Option func(*clusterClient) diff --git a/cmd/clusterctl/client/cluster/internal/dryrun/client.go b/cmd/clusterctl/client/cluster/internal/dryrun/client.go deleted file mode 100644 index d382c3d6449c..000000000000 --- a/cmd/clusterctl/client/cluster/internal/dryrun/client.go +++ /dev/null @@ -1,416 +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 dryrun - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme" -) - -var ( - localScheme = scheme.Scheme -) - -// changeTrackerID represents a unique identifier of an object. -type changeTrackerID struct { - gvk schema.GroupVersionKind - key client.ObjectKey -} - -type operationType string - -const ( - // Represents that a new object is created. - opCreate operationType = "create" - - // Represents that the object is modified. - // This could be a result of performing Patch or Update or delete and re-create operations on the object. - opModify operationType = "modify" - - // Represents that the object is deleted. - opDelete operationType = "delete" -) - -// operation represents the final effective operation and the original object (initial state) associated -// with the operation. -type operation struct { - originalValue client.Object - operation operationType -} - -// changeTracker is used to track the operations performed on the objects. -// changeTracker flattens all operations performed on the same object and -// only tracks the final effective operation on the object when compared to -// the initial state. Example: If an object is created and later modified -// it is only tracked as created (effective final operation). -// -// While changeTracker tracks the operations on objects using unique object identifiers -// ChangeSummary reports all the operations and the final state of objects. ChangeSummary -// is calculated using changeTracker and fake client. -type changeTracker struct { - changes map[changeTrackerID]*operation -} - -// Client implements a dry run Client, that is a fake.Client that logs write operations. -type Client struct { - fakeClient client.Client - apiReader client.Reader - - changeTracker *changeTracker -} - -// PatchSummary defines the patch observed on an object. -type PatchSummary struct { - // Initial state of the object. - Before *unstructured.Unstructured - // Final state of the object. - After *unstructured.Unstructured -} - -// ChangeSummary defines all the changes detected by the Dryrun execution. -// Nb. Only a single operation is reported for each object, flattening operations -// to show difference between the initial and final states. -type ChangeSummary struct { - // Created is the list of objects that are created during the dry run execution. - Created []*unstructured.Unstructured - - // Modified is the list of summary of objects that are modified (Updated, Patched and/or deleted and re-created) during the dry run execution. - Modified []*PatchSummary - - // Deleted is the list of objects that are deleted during the dry run execution. - Deleted []*unstructured.Unstructured -} - -// NewClient returns a new dry run Client. -// A dry run client mocks interactions with an api server using a fake internal object tracker. -// The objects passed will be used to initialize the fake internal object tracker when creating a new dry run client. -// If an apiReader client is passed the dry run client will use it as a fall back client for read operations (Get, List) -// when the objects are not found in the internal object tracker. Typically the apiReader passed would be a reader client -// to a real Kubernetes Cluster. -func NewClient(apiReader client.Reader, objs []client.Object) *Client { - fakeClient := fake.NewClientBuilder().WithObjects(objs...).WithStatusSubresource(&clusterv1.ClusterClass{}, &clusterv1.Cluster{}).WithScheme(localScheme).Build() - return &Client{ - fakeClient: fakeClient, - apiReader: apiReader, - changeTracker: &changeTracker{ - changes: map[changeTrackerID]*operation{}, - }, - } -} - -// Get retrieves an object for the given object key from the internal object tracker. -// If the object does not exist in the internal object tracker it tries to fetch the object -// from the Kubernetes Cluster using the apiReader client (if apiReader is not nil). -func (c *Client) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { - if err := c.fakeClient.Get(ctx, key, obj, opts...); err != nil { - // If the object is not found by the fake client, get the object - // using the apiReader. - if apierrors.IsNotFound(err) && c.apiReader != nil { - return c.apiReader.Get(ctx, key, obj, opts...) - } - return err - } - return nil -} - -// List retrieves list of objects for a given namespace and list options. -// List function returns the union of the lists from the internal object tracker and the Kubernetes Cluster. -// Nb. For objects that exist both in the internal object tracker and the Kubernetes Cluster, internal object tracker -// takes precedence. -func (c *Client) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { - var gvk schema.GroupVersionKind - if uList, ok := list.(*unstructured.UnstructuredList); ok { - gvk = uList.GroupVersionKind() - } else { - var err error - gvk, err = apiutil.GVKForObject(list, c.fakeClient.Scheme()) - if err != nil { - return errors.Wrap(err, "failed to get GVK of target object") - } - } - - // Fetch lists from both fake client and the apiReader and merge the two lists. - unstructuredFakeList := &unstructured.UnstructuredList{} - unstructuredFakeList.SetGroupVersionKind(gvk) - if err := c.fakeClient.List(ctx, unstructuredFakeList, opts...); err != nil { - return err - } - if c.apiReader != nil { - unstructuredReaderList := &unstructured.UnstructuredList{} - unstructuredReaderList.SetGroupVersionKind(gvk) - if err := c.apiReader.List(ctx, unstructuredReaderList, opts...); err != nil { - return err - } - mergeLists(unstructuredFakeList, unstructuredReaderList) - } - - if err := c.Scheme().Convert(unstructuredFakeList, list, nil); err != nil { - return errors.Wrapf(err, "failed to convert unstructured list to %T", list) - } - return nil -} - -// Create saves the object in the internal object tracker. -func (c *Client) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { - err := c.fakeClient.Create(ctx, obj, opts...) - if err == nil { - id := trackerIDFor(obj) - // If the object was previously deleted, it is now being re-created. Effectively it is a modify operation - // on the object. - if op, ok := c.changeTracker.changes[id]; ok { - if op.operation == opDelete { - op.operation = opModify - } - } else { - // This is the first operation on this object. Track the create operation. - c.changeTracker.changes[id] = &operation{ - operation: opCreate, - } - } - } - return err -} - -// Delete deletes the given obj from internal object tracker. -// Delete will not affect objects in the Kubernetes Cluster. -func (c *Client) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { - err := c.fakeClient.Delete(ctx, obj, opts...) - if err != nil { - if !apierrors.IsNotFound(err) { - return err - } - if c.apiReader == nil { - return err - } - // It is possible that we are trying to delete an object that exists in the Kubernetes Cluster but - // not in the internal object tracker. - // In such cases, check if the underlying object exists and if the object does - // not exist return the original error. - tmpObj := obj.DeepCopyObject().(client.Object) - if getErr := c.apiReader.Get(ctx, client.ObjectKeyFromObject(obj), tmpObj); getErr != nil { - if apierrors.IsNotFound(getErr) { - // Delete was called on an object that does no exists in the internal object tracker and in the - // Kubernetes Cluster. Return error. - // Note: return the original delete error. Not the get error. - return err - } - return errors.Wrap(err, "failed to check if object exists in underlying cluster") - } - } - // If the object is already tracked under a different operation we need to adjust the effective - // operation using the following rules: - // - If the object is tracked as created, drop the tracking. Effective operation is object never existed. - // - If the object is tracked in modified, change to deleted. Effective operation is object is deleted. - id := trackerIDFor(obj) - if op, ok := c.changeTracker.changes[id]; ok { - if op.operation == opCreate { - delete(c.changeTracker.changes, id) - } - if op.operation == opModify { - op.operation = opDelete - } - } else { - // The object is observed for the first time. - // Track the delete operation on the object. - c.changeTracker.changes[id] = &operation{ - originalValue: obj, - operation: opDelete, - } - } - return nil -} - -// Update updates the given obj in the internal object tracker. -// NOTE: Topology reconciler does not use update, so we are skipping implementation for now. -func (c *Client) Update(_ context.Context, _ client.Object, _ ...client.UpdateOption) error { - panic("Update method is not supported by the dryrun client") -} - -// Patch patches the given obj in the internal object tracker. -// The patch operation will be tracked if the object does not exist in the internal object tracker but exists in the Kubernetes Cluster. -func (c *Client) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { - originalObj := obj.DeepCopyObject().(client.Object) - // The fake client Patch operation internally makes a Get call. Therefore, - // create the object if it does not exist in the fake object tracker using the fake client. - // Note: Because of this operation we will nullify any real errors caused by calling Patch on an object that does no exist. - // Such cases can only occur because of bugs in reconciler. The dry run operation is not meant to capture bugs in the reconciler - // hence we choose to ignore such edge cases. - if err := c.ensureObjInFakeClient(ctx, obj); err != nil { - return errors.Wrap(err, "failed to ensure object is available in fake object tracker") - } - err := c.fakeClient.Patch(ctx, obj, patch, opts...) - if err == nil { - id := trackerIDFor(obj) - // If the object is not already tracked, track the modify operation. - // If the object is already tracked we don't need to perform any further action because of the following: - // - Tracked as created - created takes precedence over modified. - // - Tracked as modified - the object is already tracked with the correct operation. - // - Tracked as deleted - case not possible. Object cannot be patched after it is deleted. - if _, ok := c.changeTracker.changes[id]; !ok { - c.changeTracker.changes[id] = &operation{ - originalValue: originalObj, - operation: opModify, - } - } - } - return err -} - -// DeleteAllOf deletes all objects of the given type matching the given options. -// NOTE: Topology reconciler does not use DeleteAllOf, so we are skipping implementation for now. -func (c *Client) DeleteAllOf(_ context.Context, _ client.Object, _ ...client.DeleteAllOfOption) error { - panic("DeleteAllOf method is not supported by the dryrun client") -} - -// Status returns a client which can update the status subresource for Kubernetes objects. -func (c *Client) Status() client.StatusWriter { - return c.fakeClient.Status() -} - -// Scheme returns the scheme this client is using. -func (c *Client) Scheme() *runtime.Scheme { - return c.fakeClient.Scheme() -} - -// RESTMapper returns the rest this client is using. -func (c *Client) RESTMapper() meta.RESTMapper { - return c.fakeClient.RESTMapper() -} - -// SubResource returns the sub resource this client is using. -func (c *Client) SubResource(subResource string) client.SubResourceClient { - return c.fakeClient.SubResource(subResource) -} - -// GroupVersionKindFor returns the GroupVersionKind for the given object. -func (c *Client) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { - return c.fakeClient.GroupVersionKindFor(obj) -} - -// IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced. -func (c *Client) IsObjectNamespaced(obj runtime.Object) (bool, error) { - return c.fakeClient.IsObjectNamespaced(obj) -} - -// Changes generates a summary of all the changes observed from the creation of the dry run client -// to when this function is called. -func (c *Client) Changes(ctx context.Context) (*ChangeSummary, error) { - changes := &ChangeSummary{ - Created: []*unstructured.Unstructured{}, - Modified: []*PatchSummary{}, - Deleted: []*unstructured.Unstructured{}, - } - - for id, op := range c.changeTracker.changes { - switch op.operation { - case opCreate: - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(id.gvk) - obj.SetNamespace(id.key.Namespace) - obj.SetName(id.key.Name) - if err := c.fakeClient.Get(ctx, id.key, obj); err != nil { - return nil, errors.Wrapf(err, "failed to read created object %s", id.key.String()) - } - changes.Created = append(changes.Created, obj) - case opModify: - // Get the final object. - after := &unstructured.Unstructured{} - after.SetGroupVersionKind(id.gvk) - after.SetNamespace(id.key.Namespace) - after.SetName(id.key.Name) - if err := c.fakeClient.Get(ctx, id.key, after); err != nil { - return nil, errors.Wrapf(err, "failed to read modified object %s", id.key.String()) - } - // Get the initial object. - before := &unstructured.Unstructured{} - if err := c.Scheme().Convert(op.originalValue, before, nil); err != nil { - return nil, errors.Wrapf(err, "failed to convert %s to unstructured", client.ObjectKeyFromObject(op.originalValue).String()) - } - changes.Modified = append(changes.Modified, &PatchSummary{ - Before: before, - After: after, - }) - case opDelete: - obj := &unstructured.Unstructured{} - if err := c.Scheme().Convert(op.originalValue, obj, nil); err != nil { - return nil, errors.Wrapf(err, "failed to convert %s to unstructured", client.ObjectKeyFromObject(op.originalValue).String()) - } - changes.Deleted = append(changes.Deleted, obj) - default: - return nil, fmt.Errorf("untracked operation detected") - } - } - - return changes, nil -} - -// ensureObjInFakeClient makes sure that the object is available in the fake client. -// If the object is not already available it will add it to the fake client by running a "Create" -// operation. -func (c *Client) ensureObjInFakeClient(ctx context.Context, obj client.Object) error { - o := obj.DeepCopyObject().(client.Object) - // During create object should not have resourceVersion. - o.SetResourceVersion("") - if err := c.fakeClient.Create(ctx, o); err != nil { - if apierrors.IsAlreadyExists(err) { - // If the object already exists it is okay for create to fail. - return nil - } - return errors.Wrap(err, "failed to add object to fake object tracker") - } - return nil -} - -// mergeLists merges the 2 lists a and b by adding every item in b -// that is not in a to list a. -// List a will be merged list. -func mergeLists(a, b *unstructured.UnstructuredList) { - keyGen := func(u *unstructured.Unstructured) string { - return fmt.Sprintf("%s-%s", u.GroupVersionKind().String(), client.ObjectKeyFromObject(u).String()) - } - keys := map[string]bool{} - // Generate all unique keys for the items in list a. - for i := range a.Items { - keys[keyGen(&a.Items[i])] = true - } - // For every item in b that is not in a add it to a. - for i := range b.Items { - if _, ok := keys[keyGen(&b.Items[i])]; !ok { - a.Items = append(a.Items, b.Items[i]) - } - } -} - -func trackerIDFor(o client.Object) changeTrackerID { - return changeTrackerID{ - gvk: o.GetObjectKind().GroupVersionKind(), - key: client.ObjectKeyFromObject(o), - } -} diff --git a/cmd/clusterctl/client/cluster/internal/dryrun/doc.go b/cmd/clusterctl/client/cluster/internal/dryrun/doc.go deleted file mode 100644 index a7db64da68ef..000000000000 --- a/cmd/clusterctl/client/cluster/internal/dryrun/doc.go +++ /dev/null @@ -1,18 +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 dryrun implements clusterctl dryrun functionality. -package dryrun diff --git a/cmd/clusterctl/client/cluster/topology.go b/cmd/clusterctl/client/cluster/topology.go deleted file mode 100644 index 070b5340a4b7..000000000000 --- a/cmd/clusterctl/client/cluster/topology.go +++ /dev/null @@ -1,855 +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 cluster - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/component-base/featuregate" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - crwebhook "sigs.k8s.io/controller-runtime/pkg/webhook" - - clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster/internal/dryrun" - "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme" - logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" - "sigs.k8s.io/cluster-api/feature" - clusterclasscontroller "sigs.k8s.io/cluster-api/internal/controllers/clusterclass" - clustertopologycontroller "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster" - "sigs.k8s.io/cluster-api/internal/webhooks" - "sigs.k8s.io/cluster-api/util/contract" -) - -const ( - maxClusterPerInput = 1 - maxClusterClassesPerInput = 1 -) - -// TopologyClient has methods to work with ClusterClass and ManagedTopologies. -type TopologyClient interface { - Plan(ctx context.Context, in *TopologyPlanInput) (*TopologyPlanOutput, error) -} - -// topologyClient implements TopologyClient. -type topologyClient struct { - proxy Proxy - inventoryClient InventoryClient -} - -// ensure topologyClient implements TopologyClient. -var _ TopologyClient = &topologyClient{} - -// newTopologyClient returns a TopologyClient. -func newTopologyClient(proxy Proxy, inventoryClient InventoryClient) TopologyClient { - return &topologyClient{ - proxy: proxy, - inventoryClient: inventoryClient, - } -} - -// TopologyPlanInput defines the input for the Plan function. -type TopologyPlanInput struct { - Objs []*unstructured.Unstructured - TargetClusterName string - TargetNamespace string -} - -// PatchSummary defined the patch observed on an object. -type PatchSummary = dryrun.PatchSummary - -// ChangeSummary defines all the changes detected by the plan operation. -type ChangeSummary = dryrun.ChangeSummary - -// TopologyPlanOutput defines the output of the Plan function. -type TopologyPlanOutput struct { - // Clusters is the list clusters affected by the input. - Clusters []client.ObjectKey - // ClusterClasses is the list of clusters affected by the input. - ClusterClasses []client.ObjectKey - // ReconciledCluster is the cluster on which the topology reconciler loop is executed. - // If there is only one affected cluster then it becomes the ReconciledCluster. If not, - // the ReconciledCluster is chosen using additional information in the TopologyPlanInput. - // ReconciledCluster can be empty if no single target cluster is provided. - ReconciledCluster *client.ObjectKey - // ChangeSummary is the full list of changes (objects created, modified and deleted) observed - // on the ReconciledCluster. ChangeSummary is empty if ReconciledCluster is empty. - *ChangeSummary -} - -// Plan performs a dry run execution of the topology reconciler using the given inputs. -// It returns a summary of the changes observed during the execution. -func (t *topologyClient) Plan(ctx context.Context, in *TopologyPlanInput) (*TopologyPlanOutput, error) { - log := logf.Log - - // Make sure the inputs are valid. - if err := t.validateInput(in); err != nil { - return nil, errors.Wrap(err, "input failed validation") - } - - // If there is a reachable apiserver with CAPI installed fetch a client for the server. - // This client will be used as a fall back client when looking for objects that are not - // in the input. - // Example: This client will be used to fetch the underlying ClusterClass when the input - // only has a Cluster object. - var c client.Client - if err := t.proxy.CheckClusterAvailable(ctx); err == nil { - if initialized, err := t.inventoryClient.CheckCAPIInstalled(ctx); err == nil && initialized { - c, err = t.proxy.NewClient(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to create a client to the cluster") - } - log.Info("Detected a cluster with Cluster API installed. Will use it to fetch missing objects.") - } - } - - // Prepare the inputs for dry running the reconciler. This includes steps like setting missing namespaces on objects - // and adjusting cluster objects to reflect updated state. - if err := t.prepareInput(ctx, in, c); err != nil { - return nil, errors.Wrap(err, "failed preparing input") - } - - // Run defaulting and validation on core CAPI objects - Cluster and ClusterClasses. - // This mimics the defaulting and validation webhooks that will run on the objects during a real execution. - // Running defaulting and validation on these objects helps to improve the UX of using the plan operation. - // This is especially important when working with Clusters and ClusterClasses that use variable and patches. - reconciledClusterClasses, err := t.runDefaultAndValidationWebhooks(ctx, in, c) - if err != nil { - return nil, errors.Wrap(err, "failed defaulting and validation on input objects") - } - - objs := []client.Object{} - // Add all the objects from the input to the list used when initializing the dry run client. - for _, o := range filterObjects(in.Objs, clusterv1.GroupVersion.WithKind("ClusterClass")) { - objs = append(objs, o) - } - // Note: We have to add the reconciled ClusterClasses, because the Cluster reconciler depends on that. - objs = append(objs, reconciledClusterClasses...) - // Add mock CRDs of all the provider objects in the input to the list used when initializing the dry run client. - // Adding these CRDs makes sure that UpdateReferenceAPIContract calls in the reconciler can work. - for _, o := range t.generateCRDs(in.Objs) { - objs = append(objs, o) - } - - dryRunClient := dryrun.NewClient(c, objs) - // Calculate affected ClusterClasses. - affectedClusterClasses, err := t.affectedClusterClasses(ctx, in, dryRunClient) - if err != nil { - return nil, errors.Wrap(err, "failed calculating affected ClusterClasses") - } - // Calculate affected Clusters. - affectedClusters, err := t.affectedClusters(ctx, in, dryRunClient) - if err != nil { - return nil, errors.Wrap(err, "failed calculating affected Clusters") - } - - res := &TopologyPlanOutput{ - Clusters: affectedClusters, - ClusterClasses: affectedClusterClasses, - ChangeSummary: &dryrun.ChangeSummary{}, - } - - // Calculate the target cluster object. - // Full changeset is only generated for the target cluster. - var targetCluster *client.ObjectKey - if in.TargetClusterName != "" { - // Check if the target cluster is among the list of affected clusters and use that. - target := client.ObjectKey{ - Namespace: in.TargetNamespace, - Name: in.TargetClusterName, - } - if inList(affectedClusters, target) { - targetCluster = &target - } else { - return nil, fmt.Errorf("target cluster %q is not among the list of affected clusters", target.String()) - } - } else if len(affectedClusters) == 1 { - // If no target cluster is specified and if there is only one affected cluster, use that as the target cluster. - targetCluster = &affectedClusters[0] - } - - if targetCluster == nil { - // There is no target cluster, return here. We will - // not generate a full change summary. - return res, nil - } - - res.ReconciledCluster = targetCluster - reconciler := &clustertopologycontroller.Reconciler{ - Client: dryRunClient, - APIReader: dryRunClient, - } - reconciler.SetupForDryRun(&noOpRecorder{}) - request := reconcile.Request{NamespacedName: *targetCluster} - // Run the topology reconciler. - if _, err := reconciler.Reconcile(ctx, request); err != nil { - return nil, errors.Wrap(err, "failed to dry run the topology controller") - } - // Calculate changes observed by dry run client. - changes, err := dryRunClient.Changes(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to get changes made by the topology controller") - } - res.ChangeSummary = changes - - return res, nil -} - -// validateInput checks that the topology plan input does not violate any of the below expectations: -// - no more than 1 cluster in the input. -// - no more than 1 clusterclass in the input. -func (t *topologyClient) validateInput(in *TopologyPlanInput) error { - // Check if objects of the same Group.Kind are using the same version. - // Note: This is because the dryrun client does not guarantee any coversions. - if !hasUniqueVersionPerGroupKind(in.Objs) { - return fmt.Errorf("objects of the same Group.Kind should use the same apiVersion") - } - - // Check all the objects in the input belong to the same namespace. - // Note: It is okay if all the objects in the input do not have any namespace. - // In such case, the list of unique namespaces will be [""]. - namespaces := uniqueNamespaces(in.Objs) - if len(namespaces) != 1 { - return fmt.Errorf("all the objects in the input should belong to the same namespace") - } - - ns := namespaces[0] - // If the objects have a non empty namespace make sure that it matches the TargetNamespace. - if ns != "" && in.TargetNamespace != "" && ns != in.TargetNamespace { - return fmt.Errorf("the namespace from the provided object(s) %q does not match the namespace %q", ns, in.TargetNamespace) - } - in.TargetNamespace = ns - - clusterCount, clusterClassCount := len(getClusters(in.Objs)), len(getClusterClasses(in.Objs)) - if clusterCount > maxClusterPerInput || clusterClassCount > maxClusterClassesPerInput { - return fmt.Errorf( - "input should have at most %d Cluster(s) and at most %d ClusterClass(es). Found %d Cluster(s) and %d ClusterClass(es)", - maxClusterPerInput, - maxClusterClassesPerInput, - clusterCount, - clusterClassCount, - ) - } - return nil -} - -// prepareInput does the following on the input objects: -// - Set the target namespace on the objects if not set (this operation is generally done by kubectl) -// - Prepare cluster objects so that the state of the cluster, if modified, correctly represents -// the expected changes. -func (t *topologyClient) prepareInput(ctx context.Context, in *TopologyPlanInput, apiReader client.Reader) error { - if err := t.setMissingNamespaces(ctx, in.TargetNamespace, in.Objs); err != nil { - return errors.Wrap(err, "failed to set missing namespaces") - } - - if err := t.prepareClusters(ctx, getClusters(in.Objs), apiReader); err != nil { - return errors.Wrap(err, "failed to prepare clusters") - } - return nil -} - -// setMissingNamespaces sets the object to the current namespace on objects -// that are missing the namespace field. -func (t *topologyClient) setMissingNamespaces(ctx context.Context, currentNamespace string, objs []*unstructured.Unstructured) error { - if currentNamespace == "" { - // If TargetNamespace is not provided use "default" namespace. - currentNamespace = metav1.NamespaceDefault - // If a cluster is available use the current namespace as defined in its kubeconfig. - if err := t.proxy.CheckClusterAvailable(ctx); err == nil { - currentNamespace, err = t.proxy.CurrentNamespace() - if err != nil { - return errors.Wrap(err, "failed to get current namespace") - } - } - } - // Set namespace on objects that do not have namespace value. - // Skip Namespace objects, as they are non-namespaced. - for i := range objs { - isNamespace := objs[i].GroupVersionKind().Kind == namespaceKind - if objs[i].GetNamespace() == "" && !isNamespace { - objs[i].SetNamespace(currentNamespace) - } - } - return nil -} - -// prepareClusters does the following operations on each Cluster in the input. -// - Check if the Cluster exists in the real apiserver. -// - If the Cluster exists in the real apiserver we merge the object from the -// server with the object from the input. This final object correctly represents the -// modified cluster object. -// Note: We are using a simple 2-way merge to calculate the final object in this function -// to keep the function simple. In reality kubectl does a lot more. This function does not behave exactly -// the same way as kubectl does. -// -// *Important note*: We do this above operation because the topology reconciler in a -// -// real run takes as input a cluster object from the apiserver that has merged spec of -// the changes in the input and the one stored in the server. For example: the cluster -// object in the input will not have cluster.spec.infrastructureRef and cluster.spec.controlPlaneRef -// but the merged object will have these fields set. -func (t *topologyClient) prepareClusters(ctx context.Context, clusters []*unstructured.Unstructured, apiReader client.Reader) error { - if apiReader == nil { - // If there is no backing server there is nothing more to do here. - // Return early. - return nil - } - - // For a Cluster check if it already exists in the server. If it does, get the object from the server - // and merge it with the Cluster from the file to get effective 'modified' Cluster. - for _, cluster := range clusters { - storedCluster := &clusterv1.Cluster{} - if err := apiReader.Get( - ctx, - client.ObjectKey{Namespace: cluster.GetNamespace(), Name: cluster.GetName()}, - storedCluster, - ); err != nil { - if apierrors.IsNotFound(err) { - // The Cluster does not exist in the server. Nothing more to do here. - continue - } - return errors.Wrapf(err, "failed to get Cluster %s/%s", cluster.GetNamespace(), cluster.GetName()) - } - originalJSON, err := json.Marshal(storedCluster) - if err != nil { - return errors.Wrapf(err, "failed to convert Cluster %s/%s to json", storedCluster.Namespace, storedCluster.Name) - } - modifiedJSON, err := json.Marshal(cluster) - if err != nil { - return errors.Wrapf(err, "failed to convert Cluster %s/%s", cluster.GetNamespace(), cluster.GetName()) - } - // Apply the modified object to the original one, merging the values of both; - // in case of conflicts, values from the modified object are preserved. - originalWithModifiedJSON, err := jsonpatch.MergePatch(originalJSON, modifiedJSON) - if err != nil { - return errors.Wrap(err, "failed to apply modified json to original json") - } - if err := json.Unmarshal(originalWithModifiedJSON, &cluster.Object); err != nil { - return errors.Wrap(err, "failed to convert modified json to object") - } - } - return nil -} - -// runDefaultAndValidationWebhooks runs the defaulting and validation webhooks on the -// ClusterClass and Cluster objects in the input thus replicating the real kube-apiserver flow -// when applied. -// Nb. Perform ValidateUpdate only if the object is already in the cluster. In all other cases, -// ValidateCreate is performed. -// *Important Note*: We cannot perform defaulting and validation on provider objects as we do not have access to -// that code. -func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in *TopologyPlanInput, apiReader client.Reader) ([]client.Object, error) { - // Enable the ClusterTopology feature gate so that the defaulter and validators do not complain. - // Note: We don't need to disable it later because the CLI is short lived. - if err := feature.Gates.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", feature.ClusterTopology, true)); err != nil { - return nil, errors.Wrapf(err, "failed to enable %s feature gate", feature.ClusterTopology) - } - - // From the inputs gather all the objects that are not Clusters or ClusterClasses. - // These objects will be used when initializing a dryrun client to use in the webhooks. - filteredObjs := filterObjects( - in.Objs, - clusterv1.GroupVersion.WithKind("ClusterClass"), - clusterv1.GroupVersion.WithKind("Cluster"), - ) - objs := []client.Object{} - for _, o := range filteredObjs { - objs = append(objs, o) - } - // Creating a dryrun client will a fall back to the apiReader client (client to the underlying Kubernetes cluster) - // allows the defaulting and validation webhooks to complete actions to could potentially depend on other objects in the cluster. - // Example: Validation of cluster objects will use the client to read ClusterClasses. - webhookClient := dryrun.NewClient(apiReader, objs) - - // Run defaulting and validation on ClusterClasses. - ccWebhook := &webhooks.ClusterClass{Client: webhookClient} - if err := t.defaultAndValidateObjs( - ctx, - getClusterClasses(in.Objs), - &clusterv1.ClusterClass{}, - ccWebhook, - ccWebhook, - apiReader, - ); err != nil { - return nil, errors.Wrap(err, "failed to run defaulting and validation on ClusterClasses") - } - - // From the inputs gather all the objects that are not Clusters or ClusterClasses. - // These objects will be used when initializing a dryrun client to use in the webhooks. - filteredObjs = filterObjects( - in.Objs, - clusterv1.GroupVersion.WithKind("Cluster"), - clusterv1.GroupVersion.WithKind("ClusterClass"), - ) - - objs = []client.Object{} - for _, o := range filteredObjs { - objs = append(objs, o) - } - // Reconcile the ClusterClasses and add the reconciled version of them to the webhook client. - // This is required as validation of Cluster objects might need access to ClusterClass objects that are in the input. - // Cluster variable defaulting and validation relies on the ClusterClass `.status.variables` which is added - // during ClusterClass reconciliation. - reconciledClusterClasses, err := t.reconcileClusterClasses(ctx, in.Objs, apiReader) - if err != nil { - return nil, errors.Wrapf(err, "failed to reconcile ClusterClasses for defaulting and validating") - } - objs = append(objs, reconciledClusterClasses...) - - webhookClient = dryrun.NewClient(apiReader, objs) - - // Run defaulting and validation on Clusters. - clusterWebhook := &webhooks.Cluster{Client: webhookClient} - if err := t.defaultAndValidateObjs( - ctx, - getClusters(in.Objs), - &clusterv1.Cluster{}, - clusterWebhook, - clusterWebhook, - apiReader, - ); err != nil { - return nil, errors.Wrap(err, "failed to run defaulting and validation on Clusters") - } - - return reconciledClusterClasses, nil -} - -func (t *topologyClient) reconcileClusterClasses(ctx context.Context, inputObjects []*unstructured.Unstructured, apiReader client.Reader) ([]client.Object, error) { - reconciliationObjects := []client.Object{} - // From the inputs gather all the objects that are not ClusterClasses. - // These objects will be used when initializing a dryrun client to use in the reconciler. - for _, o := range filterObjects(inputObjects, clusterv1.GroupVersion.WithKind("ClusterClass")) { - reconciliationObjects = append(reconciliationObjects, o) - } - // Add mock CRDs of all the provider objects in the input to the list used when initializing the client. - // Adding these CRDs makes sure that UpdateReferenceAPIContract calls in the reconciler can work. - for _, o := range t.generateCRDs(inputObjects) { - reconciliationObjects = append(reconciliationObjects, o) - } - - // Create a list of all ClusterClasses, including those in the dry run input and those in the management Cluster - // API Server. - allClusterClasses := []client.Object{} - ccList := &clusterv1.ClusterClassList{} - // If an APIReader is available add the ClusterClasses from the management cluster - if apiReader != nil { - if err := apiReader.List(ctx, ccList); err != nil { - return nil, errors.Wrap(err, "failed to find ClusterClasses to default and validate Clusters") - } - for i := range ccList.Items { - allClusterClasses = append(allClusterClasses, &ccList.Items[i]) - } - } - - // Add ClusterClasses from the input - inClusterClasses := getClusterClasses(inputObjects) - cc := clusterv1.ClusterClass{} - for _, class := range inClusterClasses { - if err := scheme.Scheme.Convert(class, &cc, ctx); err != nil { - return nil, errors.Wrapf(err, "failed to convert object %s/%s to ClusterClass", class.GetNamespace(), class.GetName()) - } - allClusterClasses = append(allClusterClasses, &cc) - } - - // Each ClusterClass should be reconciled in order to ensure variables are correctly added to `status.variables`. - // This is required as Clusters are validated based of variable definitions in the ClusterClass `.status.variables`. - reconciledClusterClasses := []client.Object{} - for _, class := range allClusterClasses { - reconciledClusterClass, err := reconcileClusterClass(ctx, apiReader, class, reconciliationObjects) - if err != nil { - return nil, errors.Wrapf(err, "ClusterClass %s could not be reconciled for dry run", class.GetName()) - } - reconciledClusterClasses = append(reconciledClusterClasses, reconciledClusterClass) - } - - // Remove the ClusterClasses from the input objects and replace them with the reconciled version. - for i, obj := range inputObjects { - if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("ClusterClass") { - // remove the clusterclass from the list of reconciled clusterclasses if it was not in the input. - inputObjects = append(inputObjects[:i], inputObjects[i+1:]...) - } - } - for _, class := range reconciledClusterClasses { - obj := &unstructured.Unstructured{} - if err := localScheme.Convert(class, obj, nil); err != nil { - return nil, errors.Wrapf(err, "failed to convert %s to object", obj.GetKind()) - } - inputObjects = append(inputObjects, obj) - } - - // Return a list of successfully reconciled ClusterClasses. - return reconciledClusterClasses, nil -} - -func reconcileClusterClass(ctx context.Context, apiReader client.Reader, class client.Object, reconciliationObjects []client.Object) (*unstructured.Unstructured, error) { - targetClusterClass := client.ObjectKey{Namespace: class.GetNamespace(), Name: class.GetName()} - reconciliationObjects = append(reconciliationObjects, class) - - // Create a reconcilerClient that has access to all of the necessary templates to complete a successful reconcile - // of the ClusterClass. - reconcilerClient := dryrun.NewClient(apiReader, reconciliationObjects) - - clusterClassReconciler := &clusterclasscontroller.Reconciler{ - Client: reconcilerClient, - } - - // The first only reconciles the paused condition. - if _, err := clusterClassReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: targetClusterClass}); err != nil { - return nil, errors.Wrap(err, "failed to dry run the ClusterClass controller to reconcile the paused condition") - } - - if _, err := clusterClassReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: targetClusterClass}); err != nil { - return nil, errors.Wrap(err, "failed to dry run the ClusterClass controller") - } - - // Pull the reconciled ClusterClass using the reconcilerClient, and return the version with the updated status. - reconciledClusterClass := &clusterv1.ClusterClass{} - if err := reconcilerClient.Get(ctx, targetClusterClass, reconciledClusterClass); err != nil { - return nil, fmt.Errorf("could not retrieve ClusterClass") - } - - obj := &unstructured.Unstructured{} - // Converted the defaulted and validated object back into unstructured. - // Note: This step also makes sure that modified object is updated into the - // original unstructured object. - if err := localScheme.Convert(reconciledClusterClass, obj, nil); err != nil { - return nil, errors.Wrapf(err, "failed to convert %s to object", obj.GetKind()) - } - - return obj, nil -} - -func (t *topologyClient) defaultAndValidateObjs(ctx context.Context, objs []*unstructured.Unstructured, o client.Object, defaulter crwebhook.CustomDefaulter, validator crwebhook.CustomValidator, apiReader client.Reader) error { - for _, obj := range objs { - // The defaulter and validator need a typed object. Convert the unstructured obj to a typed object. - object := o.DeepCopyObject().(client.Object) // object here is a typed object. - if err := localScheme.Convert(obj, object, nil); err != nil { - return errors.Wrapf(err, "failed to convert object to %s", obj.GetKind()) - } - - // Perform Defaulting - if err := defaulter.Default(ctx, object); err != nil { - return errors.Wrapf(err, "failed defaulting of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName()) - } - - var oldObject client.Object - if apiReader != nil { - tmpObj := o.DeepCopyObject().(client.Object) - if err := apiReader.Get(ctx, client.ObjectKeyFromObject(obj), tmpObj); err != nil { - if !apierrors.IsNotFound(err) { - return errors.Wrapf(err, "failed to get object %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName()) - } - } else { - oldObject = tmpObj - } - } - if oldObject != nil { - if _, err := validator.ValidateUpdate(ctx, oldObject, object); err != nil { - return errors.Wrapf(err, "failed validation of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName()) - } - } else { - if _, err := validator.ValidateCreate(ctx, object); err != nil { - return errors.Wrapf(err, "failed validation of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName()) - } - } - - // Converted the defaulted and validated object back into unstructured. - // Note: This step also makes sure that modified object is updated into the - // original unstructured object. - if err := localScheme.Convert(object, obj, nil); err != nil { - return errors.Wrapf(err, "failed to convert %s to object", obj.GetKind()) - } - } - - return nil -} - -func getClusterClasses(objs []*unstructured.Unstructured) []*unstructured.Unstructured { - res := make([]*unstructured.Unstructured, 0) - for _, obj := range objs { - if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("ClusterClass") { - res = append(res, obj) - } - } - return res -} - -func getClusters(objs []*unstructured.Unstructured) []*unstructured.Unstructured { - res := make([]*unstructured.Unstructured, 0) - for _, obj := range objs { - if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("Cluster") { - res = append(res, obj) - } - } - return res -} - -func getTemplates(objs []*unstructured.Unstructured) []*unstructured.Unstructured { - res := make([]*unstructured.Unstructured, 0) - for _, obj := range objs { - if strings.HasSuffix(obj.GetKind(), clusterv1.TemplateSuffix) { - res = append(res, obj) - } - } - return res -} - -// generateCRDs creates mock CRD objects for all the provider specific objects in the input. -// These CRD objects will be added to the dry run client for UpdateReferenceAPIContract -// to work as expected. -func (t *topologyClient) generateCRDs(objs []*unstructured.Unstructured) []*apiextensionsv1.CustomResourceDefinition { - crds := []*apiextensionsv1.CustomResourceDefinition{} - crdMap := map[string]bool{} - var gvk schema.GroupVersionKind - - for _, obj := range objs { - gvk = obj.GroupVersionKind() - if strings.HasSuffix(gvk.Group, ".cluster.x-k8s.io") && !crdMap[gvk.String()] { - crd := &apiextensionsv1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{ - APIVersion: apiextensionsv1.SchemeGroupVersion.String(), - Kind: "CustomResourceDefinition", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: contract.CalculateCRDName(gvk.Group, gvk.Kind), - Labels: map[string]string{ - // Here we assume that all the versions are compatible with the Cluster API contract version. - clusterv1.GroupVersion.String(): gvk.Version, - }, - }, - } - crds = append(crds, crd) - crdMap[gvk.String()] = true - } - } - - return crds -} - -func (t *topologyClient) affectedClusterClasses(ctx context.Context, in *TopologyPlanInput, c client.Reader) ([]client.ObjectKey, error) { - affectedClusterClasses := map[client.ObjectKey]bool{} - ccList := &clusterv1.ClusterClassList{} - if err := c.List( - ctx, - ccList, - client.InNamespace(in.TargetNamespace), - ); err != nil { - return nil, errors.Wrapf(err, "failed to list ClusterClasses in namespace %s", in.TargetNamespace) - } - - // Each of the ClusterClass that uses any of the Templates in the input is an affected ClusterClass. - for _, template := range getTemplates(in.Objs) { - for i := range ccList.Items { - if clusterClassUsesTemplate(&ccList.Items[i], objToRef(template)) { - affectedClusterClasses[client.ObjectKeyFromObject(&ccList.Items[i])] = true - } - } - } - - // All the ClusterClasses in the input are considered affected ClusterClasses. - for _, cc := range getClusterClasses(in.Objs) { - affectedClusterClasses[client.ObjectKeyFromObject(cc)] = true - } - - affectedClusterClassesList := []client.ObjectKey{} - for k := range affectedClusterClasses { - affectedClusterClassesList = append(affectedClusterClassesList, k) - } - return affectedClusterClassesList, nil -} - -func (t *topologyClient) affectedClusters(ctx context.Context, in *TopologyPlanInput, c client.Reader) ([]client.ObjectKey, error) { - affectedClusters := map[client.ObjectKey]bool{} - affectedClusterClasses, err := t.affectedClusterClasses(ctx, in, c) - if err != nil { - return nil, errors.Wrap(err, "failed to get list of affected ClusterClasses") - } - clusterList := &clusterv1.ClusterList{} - if err := c.List( - ctx, - clusterList, - client.InNamespace(in.TargetNamespace), - ); err != nil { - return nil, errors.Wrapf(err, "failed to list Clusters in namespace %s", in.TargetNamespace) - } - - // Each of the Cluster that uses the ClusterClass in the input is an affected cluster. - for _, cc := range affectedClusterClasses { - for i := range clusterList.Items { - cluster := clusterList.Items[i] - if cluster.Spec.Topology != nil && cluster.GetClassKey().Name == cc.Name { - affectedClusters[client.ObjectKeyFromObject(&clusterList.Items[i])] = true - } - } - } - - // All the Clusters in the input are considered affected Clusters. - for _, cluster := range getClusters(in.Objs) { - affectedClusters[client.ObjectKeyFromObject(cluster)] = true - } - - affectedClustersList := []client.ObjectKey{} - for k := range affectedClusters { - affectedClustersList = append(affectedClustersList, k) - } - return affectedClustersList, nil -} - -func inList(list []client.ObjectKey, target client.ObjectKey) bool { - for _, i := range list { - if i == target { - return true - } - } - return false -} - -// filterObjects returns a new list of objects after dropping all the objects that match any of the given GVKs. -func filterObjects(objs []*unstructured.Unstructured, gvks ...schema.GroupVersionKind) []*unstructured.Unstructured { - res := []*unstructured.Unstructured{} - for _, o := range objs { - skip := false - for _, gvk := range gvks { - if o.GroupVersionKind() == gvk { - skip = true - break - } - } - if !skip { - res = append(res, o) - } - } - return res -} - -type noOpRecorder struct{} - -func (nr *noOpRecorder) Event(_ runtime.Object, _, _, _ string) {} -func (nr *noOpRecorder) Eventf(_ runtime.Object, _, _, _ string, _ ...interface{}) {} -func (nr *noOpRecorder) AnnotatedEventf(_ runtime.Object, _ map[string]string, _, _, _ string, _ ...interface{}) { -} - -func objToRef(o *unstructured.Unstructured) *corev1.ObjectReference { - return &corev1.ObjectReference{ - Kind: o.GetKind(), - Namespace: o.GetNamespace(), - Name: o.GetName(), - APIVersion: o.GetAPIVersion(), - } -} - -func equalRef(a, b *corev1.ObjectReference) bool { - if a.APIVersion != b.APIVersion { - return false - } - if a.Namespace != b.Namespace { - return false - } - if a.Name != b.Name { - return false - } - if a.Kind != b.Kind { - return false - } - return true -} - -func clusterClassUsesTemplate(cc *clusterv1.ClusterClass, templateRef *corev1.ObjectReference) bool { - // Check infrastructure ref. - if equalRef(cc.Spec.Infrastructure.Ref, templateRef) { - return true - } - // Check control plane ref. - if equalRef(cc.Spec.ControlPlane.Ref, templateRef) { - return true - } - // If control plane uses machine, check it. - if cc.Spec.ControlPlane.MachineInfrastructure != nil && cc.Spec.ControlPlane.MachineInfrastructure.Ref != nil { - if equalRef(cc.Spec.ControlPlane.MachineInfrastructure.Ref, templateRef) { - return true - } - } - - for _, mdClass := range cc.Spec.Workers.MachineDeployments { - // Check bootstrap template ref. - if equalRef(mdClass.Template.Bootstrap.Ref, templateRef) { - return true - } - // Check the infrastructure ref. - if equalRef(mdClass.Template.Infrastructure.Ref, templateRef) { - return true - } - } - - for _, mpClass := range cc.Spec.Workers.MachinePools { - // Check the bootstrap ref - if equalRef(mpClass.Template.Bootstrap.Ref, templateRef) { - return true - } - // Check the infrastructure ref. - if equalRef(mpClass.Template.Infrastructure.Ref, templateRef) { - return true - } - } - - return false -} - -func uniqueNamespaces(objs []*unstructured.Unstructured) []string { - ns := sets.Set[string]{} - for _, obj := range objs { - // Namespace objects do not have metadata.namespace set, but we can add the - // name of the obj to the namespace list, as it is another unique namespace. - isNamespace := obj.GroupVersionKind().Kind == namespaceKind - if isNamespace { - ns.Insert(obj.GetName()) - continue - } - - // Note: treat empty namespace (namespace not set) as a unique namespace. - // If some have a namespace set and some do not. It is safer to consider them as - // objects from different namespaces. - ns.Insert(obj.GetNamespace()) - } - return sets.List(ns) -} - -func hasUniqueVersionPerGroupKind(objs []*unstructured.Unstructured) bool { - versionMap := map[string]string{} - for _, obj := range objs { - gvk := obj.GroupVersionKind() - gk := gvk.GroupKind().String() - if ver, ok := versionMap[gk]; ok && ver != gvk.Version { - return false - } - versionMap[gk] = gvk.Version - } - return true -} diff --git a/cmd/clusterctl/client/cluster/topology_test.go b/cmd/clusterctl/client/cluster/topology_test.go deleted file mode 100644 index f4618030e272..000000000000 --- a/cmd/clusterctl/client/cluster/topology_test.go +++ /dev/null @@ -1,467 +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 cluster - -import ( - "context" - _ "embed" - "fmt" - "strings" - "testing" - - . "github.com/onsi/gomega" - "github.com/onsi/gomega/types" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" - - "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" - utilyaml "sigs.k8s.io/cluster-api/util/yaml" -) - -var ( - //go:embed assets/topology-test/new-clusterclass-and-cluster.yaml - newClusterClassAndClusterYAML []byte - - //go:embed assets/topology-test/mock-CRDs.yaml - mockCRDsYAML []byte - - //go:embed assets/topology-test/my-cluster-class.yaml - existingMyClusterClassYAML []byte - - //go:embed assets/topology-test/existing-my-cluster.yaml - existingMyClusterYAML []byte - - //go:embed assets/topology-test/existing-my-second-cluster.yaml - existingMySecondClusterYAML []byte - - // modifiedClusterYAML changes the control plane replicas from 1 to 3. - //go:embed assets/topology-test/modified-my-cluster.yaml - modifiedMyClusterYAML []byte - - // modifiedDockerMachineTemplateYAML adds metadata to the docker machine used by the control plane template.. - //go:embed assets/topology-test/modified-CP-dockermachinetemplate.yaml - modifiedDockerMachineTemplateYAML []byte - - // modifiedDockerMachinePoolTemplateYAML adds metadata to the docker machine pool used by the control plane template.. - //go:embed assets/topology-test/modified-CP-dockermachinepooltemplate.yaml - modifiedDockerMachinePoolTemplateYAML []byte - - //go:embed assets/topology-test/objects-in-different-namespaces.yaml - objsInDifferentNamespacesYAML []byte -) - -func Test_topologyClient_Plan(t *testing.T) { - type args struct { - in *TopologyPlanInput - } - type item struct { - kind string - namespace string - namePrefix string - } - type out struct { - affectedClusters []client.ObjectKey - affectedClusterClasses []client.ObjectKey - reconciledCluster *client.ObjectKey - created []item - modified []item - deleted []item - } - tests := []struct { - name string - existingObjects []*unstructured.Unstructured - args args - want out - wantErr bool - }{ - { - name: "Input with new ClusterClass and new Cluster", - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(newClusterClassAndClusterYAML), - }, - }, - want: out{ - created: []item{ - {kind: "DockerCluster", namespace: "default", namePrefix: "my-cluster-"}, - {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-md-0-"}, - {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-md-1-"}, - {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-"}, - {kind: "DockerMachinePool", namespace: "default", namePrefix: "my-cluster-mp-0-"}, - {kind: "DockerMachinePool", namespace: "default", namePrefix: "my-cluster-mp-1-"}, - {kind: "KubeadmConfigTemplate", namespace: "default", namePrefix: "my-cluster-md-0-"}, - {kind: "KubeadmConfigTemplate", namespace: "default", namePrefix: "my-cluster-md-1-"}, - {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, - {kind: "MachineDeployment", namespace: "default", namePrefix: "my-cluster-md-0-"}, - {kind: "MachineDeployment", namespace: "default", namePrefix: "my-cluster-md-1-"}, - {kind: "MachinePool", namespace: "default", namePrefix: "my-cluster-mp-0-"}, - {kind: "MachinePool", namespace: "default", namePrefix: "my-cluster-mp-1-"}, - }, - modified: []item{ - {kind: "Cluster", namespace: "default", namePrefix: "my-cluster"}, - }, - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - return []client.ObjectKey{cluster} - }(), - affectedClusterClasses: func() []client.ObjectKey { - cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} - return []client.ObjectKey{cc} - }(), - reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, - }, - wantErr: false, - }, - { - name: "Modifying an existing Cluster", - existingObjects: mustToUnstructured( - mockCRDsYAML, - existingMyClusterClassYAML, - existingMyClusterYAML, - ), - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(modifiedMyClusterYAML), - }, - }, - want: out{ - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - return []client.ObjectKey{cluster} - }(), - affectedClusterClasses: []client.ObjectKey{}, - modified: []item{ - {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, - }, - reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, - }, - wantErr: false, - }, - { - name: "Modifying an existing DockerMachineTemplate. Template used by Control Plane of an existing Cluster.", - existingObjects: mustToUnstructured( - mockCRDsYAML, - existingMyClusterClassYAML, - existingMyClusterYAML, - ), - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(modifiedDockerMachineTemplateYAML), - }, - }, - want: out{ - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - return []client.ObjectKey{cluster} - }(), - affectedClusterClasses: func() []client.ObjectKey { - cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} - return []client.ObjectKey{cc} - }(), - modified: []item{ - {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, - }, - created: []item{ - // Modifying the DockerClusterTemplate will result in template rotation. A new template will be created - // and used by KCP. - {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-"}, - }, - reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, - }, - wantErr: false, - }, - { - name: "Modifying an existing DockerMachineTemplate. Affects multiple clusters. Target Cluster not specified.", - existingObjects: mustToUnstructured( - mockCRDsYAML, - existingMyClusterClassYAML, - existingMyClusterYAML, - existingMySecondClusterYAML, - ), - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(modifiedDockerMachineTemplateYAML), - }, - }, - want: out{ - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - cluster2 := client.ObjectKey{Namespace: "default", Name: "my-second-cluster"} - return []client.ObjectKey{cluster, cluster2} - }(), - affectedClusterClasses: func() []client.ObjectKey { - cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} - return []client.ObjectKey{cc} - }(), - modified: []item{}, - created: []item{}, - reconciledCluster: nil, - }, - wantErr: false, - }, - { - name: "Modifying an existing DockerMachinePoolTemplate. Affects multiple clusters. Target Cluster not specified.", - existingObjects: mustToUnstructured( - mockCRDsYAML, - existingMyClusterClassYAML, - existingMyClusterYAML, - existingMySecondClusterYAML, - ), - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(modifiedDockerMachinePoolTemplateYAML), - }, - }, - want: out{ - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - cluster2 := client.ObjectKey{Namespace: "default", Name: "my-second-cluster"} - return []client.ObjectKey{cluster, cluster2} - }(), - affectedClusterClasses: func() []client.ObjectKey { - cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} - return []client.ObjectKey{cc} - }(), - modified: []item{}, - created: []item{}, - reconciledCluster: nil, - }, - wantErr: false, - }, - { - name: "Modifying an existing DockerMachineTemplate. Affects multiple clusters. Target Cluster specified.", - existingObjects: mustToUnstructured( - mockCRDsYAML, - existingMyClusterClassYAML, - existingMyClusterYAML, - existingMySecondClusterYAML, - ), - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(modifiedDockerMachineTemplateYAML), - TargetClusterName: "my-cluster", - }, - }, - want: out{ - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - cluster2 := client.ObjectKey{Namespace: "default", Name: "my-second-cluster"} - return []client.ObjectKey{cluster, cluster2} - }(), - affectedClusterClasses: func() []client.ObjectKey { - cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} - return []client.ObjectKey{cc} - }(), - modified: []item{ - {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, - }, - created: []item{ - // Modifying the DockerClusterTemplate will result in template rotation. A new template will be created - // and used by KCP. - {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-"}, - }, - reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, - }, - wantErr: false, - }, - { - name: "Modifying an existing DockerMachinePoolTemplate. Affects multiple clusters. Target Cluster specified.", - existingObjects: mustToUnstructured( - mockCRDsYAML, - existingMyClusterClassYAML, - existingMyClusterYAML, - existingMySecondClusterYAML, - ), - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(modifiedDockerMachinePoolTemplateYAML), - TargetClusterName: "my-cluster", - }, - }, - want: out{ - affectedClusters: func() []client.ObjectKey { - cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} - cluster2 := client.ObjectKey{Namespace: "default", Name: "my-second-cluster"} - return []client.ObjectKey{cluster, cluster2} - }(), - affectedClusterClasses: func() []client.ObjectKey { - cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} - return []client.ObjectKey{cc} - }(), - created: []item{ - {kind: "DockerMachinePool", namespace: "default", namePrefix: "my-cluster-"}, - }, - reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, - }, - wantErr: false, - }, - { - name: "Input with objects in different namespaces should return error", - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(objsInDifferentNamespacesYAML), - }, - }, - wantErr: true, - }, - { - name: "Input with TargetNamespace different from objects in input should return error", - args: args{ - in: &TopologyPlanInput{ - Objs: mustToUnstructured(newClusterClassAndClusterYAML), - TargetNamespace: "different-namespace", - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - ctx := context.Background() - - existingObjects := []client.Object{} - for _, o := range tt.existingObjects { - existingObjects = append(existingObjects, o) - } - proxy := test.NewFakeProxy().WithClusterAvailable(true).WithObjs(fakeCAPISetupObjects()...).WithObjs(existingObjects...) - inventoryClient := newInventoryClient(proxy, nil, currentContractVersion) - tc := newTopologyClient( - proxy, - inventoryClient, - ) - - res, err := tc.Plan(ctx, tt.args.in) - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - return - } - // The plan should function should not return any error. - g.Expect(err).ToNot(HaveOccurred()) - - // Check affected ClusterClasses. - g.Expect(res.ClusterClasses).To(HaveLen(len(tt.want.affectedClusterClasses))) - for _, cc := range tt.want.affectedClusterClasses { - g.Expect(res.ClusterClasses).To(ContainElement(cc)) - } - - // Check affected Clusters. - g.Expect(res.Clusters).To(HaveLen(len(tt.want.affectedClusters))) - for _, cluster := range tt.want.affectedClusters { - g.Expect(res.Clusters).To(ContainElement(cluster)) - } - - // Check the reconciled cluster. - if tt.want.reconciledCluster == nil { - g.Expect(res.ReconciledCluster).To(BeNil()) - } else { - g.Expect(res.ReconciledCluster).NotTo(BeNil()) - g.Expect(*res.ReconciledCluster).To(BeComparableTo(*tt.want.reconciledCluster)) - } - - // Check the created objects. - for _, created := range tt.want.created { - g.Expect(res.Created).To(ContainElement(MatchTopologyPlanOutputItem(created.kind, created.namespace, created.namePrefix))) - } - - // Check the modified objects. - actualModifiedObjs := []*unstructured.Unstructured{} - for _, m := range res.Modified { - actualModifiedObjs = append(actualModifiedObjs, m.After) - } - for _, modified := range tt.want.modified { - g.Expect(actualModifiedObjs).To(ContainElement(MatchTopologyPlanOutputItem(modified.kind, modified.namespace, modified.namePrefix))) - } - - // Check the deleted objects. - for _, deleted := range tt.want.deleted { - g.Expect(res.Deleted).To(ContainElement(MatchTopologyPlanOutputItem(deleted.kind, deleted.namespace, deleted.namePrefix))) - } - }) - } -} - -func fakeCAPISetupObjects() []client.Object { - return []client.Object{ - &apiextensionsv1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"}, - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: currentContractVersion, - Storage: true, - }, - }, - }, - }, - } -} - -func MatchTopologyPlanOutputItem(kind, namespace, namePrefix string) types.GomegaMatcher { - return &topologyPlanOutputItemMatcher{kind, namespace, namePrefix} -} - -type topologyPlanOutputItemMatcher struct { - kind string - namespace string - namePrefix string -} - -func (m *topologyPlanOutputItemMatcher) Match(actual interface{}) (bool, error) { - obj := actual.(*unstructured.Unstructured) - if obj.GetKind() != m.kind { - return false, nil - } - if obj.GetNamespace() != m.namespace { - return false, nil - } - if !strings.HasPrefix(obj.GetName(), m.namePrefix) { - return false, nil - } - return true, nil -} - -func (m *topologyPlanOutputItemMatcher) FailureMessage(_ interface{}) string { - return fmt.Sprintf("Expected item Kind=%s, Namespace=%s, Name(prefix)=%s to be present", m.kind, m.namespace, m.namePrefix) -} - -func (m *topologyPlanOutputItemMatcher) NegatedFailureMessage(_ interface{}) string { - return fmt.Sprintf("Expected item Kind=%s, Namespace=%s, Name(prefix)=%s not to be present", m.kind, m.namespace, m.namePrefix) -} - -func convertToPtrSlice(objs []unstructured.Unstructured) []*unstructured.Unstructured { - res := []*unstructured.Unstructured{} - for i := range objs { - res = append(res, &objs[i]) - } - return res -} - -func mustToUnstructured(rawyamls ...[]byte) []*unstructured.Unstructured { - objects := []unstructured.Unstructured{} - for _, raw := range rawyamls { - objs, err := utilyaml.ToUnstructured(raw) - if err != nil { - panic(err) - } - objects = append(objects, objs...) - } - return convertToPtrSlice(objects) -} diff --git a/cmd/clusterctl/client/repository/components.go b/cmd/clusterctl/client/repository/components.go index 510ec70535fd..2f808ddc22f2 100644 --- a/cmd/clusterctl/client/repository/components.go +++ b/cmd/clusterctl/client/repository/components.go @@ -41,7 +41,6 @@ import ( const ( namespaceKind = "Namespace" - clusterRoleKind = "ClusterRole" clusterRoleBindingKind = "ClusterRoleBinding" roleBindingKind = "RoleBinding" certificateKind = "Certificate" diff --git a/cmd/clusterctl/client/topology.go b/cmd/clusterctl/client/topology.go deleted file mode 100644 index 648f7a92aa89..000000000000 --- a/cmd/clusterctl/client/topology.go +++ /dev/null @@ -1,64 +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 client - -import ( - "context" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" -) - -// TopologyPlanOptions define options for TopologyPlan. -type TopologyPlanOptions struct { - // Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty, - // default rules for kubeconfig discovery will be used. - Kubeconfig Kubeconfig - - // Objs is the list of objects that are input to the topology plan (dry run) operation. - // The objects can be among new/modified clusters, new/modifed ClusterClasses and new/modified templates. - Objs []*unstructured.Unstructured - - // Cluster is the name of the cluster to dryrun reconcile if multiple clusters are affected by the input. - Cluster string - - // Namespace is the target namespace for the operation. - // This namespace is used as default for objects with missing namespaces. - // If the namespace of any of the input objects conflicts with Namespace an error is returned. - Namespace string -} - -// TopologyPlanOutput defines the output of the topology plan operation. -type TopologyPlanOutput = cluster.TopologyPlanOutput - -// TopologyPlan performs a dry run execution of the topology reconciler using the given inputs. -// It returns a summary of the changes observed during the execution. -func (c *clusterctlClient) TopologyPlan(ctx context.Context, options TopologyPlanOptions) (*TopologyPlanOutput, error) { - clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) - if err != nil { - return nil, err - } - - out, err := clusterClient.Topology().Plan(ctx, &cluster.TopologyPlanInput{ - Objs: options.Objs, - TargetClusterName: options.Cluster, - TargetNamespace: options.Namespace, - }) - - return out, err -} diff --git a/cmd/clusterctl/cmd/alpha.go b/cmd/clusterctl/cmd/alpha.go index 674419b3147b..ededc9143520 100644 --- a/cmd/clusterctl/cmd/alpha.go +++ b/cmd/clusterctl/cmd/alpha.go @@ -30,7 +30,6 @@ var alphaCmd = &cobra.Command{ func init() { // Alpha commands should be added here. alphaCmd.AddCommand(rolloutCmd) - alphaCmd.AddCommand(topologyCmd) RootCmd.AddCommand(alphaCmd) } diff --git a/cmd/clusterctl/cmd/topology.go b/cmd/clusterctl/cmd/topology.go deleted file mode 100644 index 89c54ad3fc96..000000000000 --- a/cmd/clusterctl/cmd/topology.go +++ /dev/null @@ -1,27 +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 cmd - -import ( - "github.com/spf13/cobra" -) - -var topologyCmd = &cobra.Command{ - Use: "topology", - Short: "Commands for ClusterClass based clusters", - Long: `Commands for ClusterClass based clusters.`, -} diff --git a/cmd/clusterctl/cmd/topology_plan.go b/cmd/clusterctl/cmd/topology_plan.go deleted file mode 100644 index 50c063bd5a47..000000000000 --- a/cmd/clusterctl/cmd/topology_plan.go +++ /dev/null @@ -1,394 +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 cmd - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "path" - "path/filepath" - "regexp" - "sort" - "strings" - - "github.com/olekukonko/tablewriter" - pkgerrors "github.com/pkg/errors" - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/exec" - crclient "sigs.k8s.io/controller-runtime/pkg/client" - - "sigs.k8s.io/cluster-api/cmd/clusterctl/client" - "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" - "sigs.k8s.io/cluster-api/cmd/clusterctl/cmd/internal/templates" - utilyaml "sigs.k8s.io/cluster-api/util/yaml" -) - -type topologyPlanOptions struct { - kubeconfig string - kubeconfigContext string - files []string - cluster string - namespace string - outDir string -} - -var tp = &topologyPlanOptions{} - -var topologyPlanCmd = &cobra.Command{ - Use: "plan", - Short: "List the changes to clusters that use managed topologies for a given input", - Long: templates.LongDesc(` - Provide the list of objects that would be created, modified and deleted when an input file is applied. - The input can be a file with a new/modified cluster, new/modified ClusterClass, new/modified templates. - Details about the objects that will be created and modified will be stored in a path passed using --output-directory. - - This command can also be run without a real cluster. In such cases, the input should contain all the objects needed. - - Note: Among all the objects in the input defaulting and validation will be performed only for Cluster - and ClusterClasses. All other objects in the input are expected to be valid and have default values. - `), - Example: templates.Examples(` - # List all the objects that will be created and modified when creating a new cluster. - clusterctl alpha topology plan -f new-cluster.yaml -o output/ - - # List the changes when modifying a cluster. - clusterctl alpha topology plan -f modified-cluster.yaml -o output/ - - # List all the objects that will be created and modified when creating a new cluster along with a new ClusterClass. - clusterctl alpha topology plan -f new-cluster-and-cluster-class.yaml -o output/ - - # List the clusters impacted by a ClusterClass change. - clusterctl alpha topology plan -f modified-cluster-class.yaml -o output/ - - # List the changes to "cluster1" when a ClusterClass is changed. - clusterctl alpha topology plan -f modified-cluster-class.yaml --cluster "cluster1" -o output/ - - # List the clusters and ClusterClasses impacted by a template change. - clusterctl alpha topology plan -f modified-template.yaml -o output/ - `), - Args: cobra.NoArgs, - RunE: func(*cobra.Command, []string) error { - return runTopologyPlan() - }, -} - -func init() { - topologyPlanCmd.Flags().StringVar(&initOpts.kubeconfig, "kubeconfig", "", - "Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.") - topologyPlanCmd.Flags().StringVar(&initOpts.kubeconfigContext, "kubeconfig-context", "", - "Context to be used within the kubeconfig file. If empty, current context will be used.") - - topologyPlanCmd.Flags().StringArrayVarP(&tp.files, "file", "f", nil, "path to the file with new or modified resources to be applied; the file should not contain more than one Cluster or more than one ClusterClass") - topologyPlanCmd.Flags().StringVarP(&tp.cluster, "cluster", "c", "", "name of the target cluster; this parameter is required when more than one cluster is affected") - topologyPlanCmd.Flags().StringVarP(&tp.namespace, "namespace", "n", "", "target namespace for the operation. If specified, it is used as default namespace for objects with missing namespace") - topologyPlanCmd.Flags().StringVarP(&tp.outDir, "output-directory", "o", "", "output directory to write details about created/modified objects") - - if err := topologyPlanCmd.MarkFlagRequired("file"); err != nil { - panic(err) - } - if err := topologyPlanCmd.MarkFlagRequired("output-directory"); err != nil { - panic(err) - } - - topologyPlanCmd.Deprecated = "it will be removed in one of the upcoming releases.\n" - - topologyCmd.AddCommand(topologyPlanCmd) -} - -func runTopologyPlan() error { - ctx := context.Background() - - c, err := client.New(ctx, cfgFile) - if err != nil { - return err - } - - objs := []unstructured.Unstructured{} - for _, f := range tp.files { - raw, err := os.ReadFile(f) //nolint:gosec - if err != nil { - return pkgerrors.Wrapf(err, "failed to read input file %q", f) - } - objects, err := utilyaml.ToUnstructured(raw) - if err != nil { - return pkgerrors.Wrapf(err, "failed to convert file %q to list of objects", f) - } - objs = append(objs, objects...) - } - - out, err := c.TopologyPlan(ctx, client.TopologyPlanOptions{ - Kubeconfig: client.Kubeconfig{Path: tp.kubeconfig, Context: tp.kubeconfigContext}, - Objs: convertToPtrSlice(objs), - Cluster: tp.cluster, - Namespace: tp.namespace, - }) - if err != nil { - return err - } - return printTopologyPlanOutput(out, tp.outDir) -} - -func printTopologyPlanOutput(out *cluster.TopologyPlanOutput, outdir string) error { - printAffectedClusterClasses(out) - printAffectedClusters(out) - if len(out.Clusters) == 0 { - // No affected clusters. Return early. - return nil - } - if out.ReconciledCluster == nil { - fmt.Printf("No target cluster identified. Use --cluster to specify a target cluster to get detailed changes.") - } else { - printChangeSummary(out) - if err := writeOutputFiles(out, outdir); err != nil { - return pkgerrors.Wrap(err, "failed to write output files of target cluster changes") - } - } - fmt.Printf("\n") - return nil -} - -func printAffectedClusterClasses(out *cluster.TopologyPlanOutput) { - if len(out.ClusterClasses) == 0 { - // If there are no affected ClusterClasses return early. Nothing more to do here. - fmt.Printf("No ClusterClasses will be affected by the changes.\n") - return - } - fmt.Printf("The following ClusterClasses will be affected by the changes:\n") - for _, cc := range out.ClusterClasses { - fmt.Printf(" * %s/%s\n", cc.Namespace, cc.Name) - } - fmt.Printf("\n") -} - -func printAffectedClusters(out *cluster.TopologyPlanOutput) { - if len(out.Clusters) == 0 { - // if there are not affected Clusters return early. Nothing more to do here. - fmt.Printf("No Clusters will be affected by the changes.\n") - return - } - fmt.Printf("The following Clusters will be affected by the changes:\n") - for _, cluster := range out.Clusters { - fmt.Printf(" * %s/%s\n", cluster.Namespace, cluster.Name) - } - fmt.Printf("\n") -} - -func printChangeSummary(out *cluster.TopologyPlanOutput) { - if len(out.Created) == 0 && len(out.Modified) == 0 && len(out.Deleted) == 0 { - fmt.Printf("No changes detected for Cluster %q.\n", fmt.Sprintf("%s/%s", out.ReconciledCluster.Namespace, out.ReconciledCluster.Name)) - return - } - - fmt.Printf("Changes for Cluster %q: \n", fmt.Sprintf("%s/%s", out.ReconciledCluster.Namespace, out.ReconciledCluster.Name)) - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Namespace", "Kind", "Name", "Action"}) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetBorder(false) - - // Add the created rows. - sort.Slice(out.Created, func(i, j int) bool { return lessByKindAndName(out.Created[i], out.Created[j]) }) - for _, c := range out.Created { - addRow(table, c, "created", tablewriter.FgGreenColor) - } - - // Add the modified rows. - sort.Slice(out.Modified, func(i, j int) bool { return lessByKindAndName(out.Modified[i].After, out.Modified[j].After) }) - for _, m := range out.Modified { - addRow(table, m.After, "modified", tablewriter.FgYellowColor) - } - - // Add the deleted rows. - sort.Slice(out.Deleted, func(i, j int) bool { return lessByKindAndName(out.Deleted[i], out.Deleted[j]) }) - for _, d := range out.Deleted { - addRow(table, d, "deleted", tablewriter.FgRedColor) - } - fmt.Printf("\n") - table.Render() - fmt.Printf("\n") -} - -func writeOutputFiles(out *cluster.TopologyPlanOutput, outDir string) error { - if _, err := os.Stat(outDir); os.IsNotExist(err) { - return fmt.Errorf("output directory %q does not exist", outDir) - } - - // Write created files - createdDir := path.Join(outDir, "created") - if err := os.MkdirAll(createdDir, 0750); err != nil { - return pkgerrors.Wrapf(err, "failed to create %q directory", createdDir) - } - for _, c := range out.Created { - yaml, err := utilyaml.FromUnstructured([]unstructured.Unstructured{*c}) - if err != nil { - return pkgerrors.Wrap(err, "failed to convert object to yaml") - } - fileName := fmt.Sprintf("%s_%s_%s.yaml", c.GetKind(), c.GetNamespace(), c.GetName()) - filePath := path.Join(createdDir, fileName) - if err := os.WriteFile(filePath, yaml, 0600); err != nil { - return pkgerrors.Wrapf(err, "failed to write yaml to file %q", filePath) - } - } - if len(out.Created) != 0 { - fmt.Printf("Created objects are written to directory %q\n", createdDir) - } - - // Write modified files - modifiedDir := path.Join(outDir, "modified") - if err := os.MkdirAll(modifiedDir, 0750); err != nil { - return pkgerrors.Wrapf(err, "failed to create %q directory", modifiedDir) - } - for _, m := range out.Modified { - // Write the modified object to file. - fileNameModified := fmt.Sprintf("%s_%s_%s.modified.yaml", m.After.GetKind(), m.After.GetNamespace(), m.After.GetName()) - filePathModified := path.Join(modifiedDir, fileNameModified) - if err := writeObjectToFile(filePathModified, m.After); err != nil { - return pkgerrors.Wrap(err, "failed to write modified object to file") - } - - // Write the original object to file. - fileNameOriginal := fmt.Sprintf("%s_%s_%s.original.yaml", m.Before.GetKind(), m.Before.GetNamespace(), m.Before.GetName()) - filePathOriginal := path.Join(modifiedDir, fileNameOriginal) - if err := writeObjectToFile(filePathOriginal, m.Before); err != nil { - return pkgerrors.Wrap(err, "failed to write original object to file") - } - - // Calculate the jsonpatch and write to a file. - patch := crclient.MergeFrom(m.Before) - jsonPatch, err := patch.Data(m.After) - if err != nil { - return pkgerrors.Wrapf(err, "failed to calculate jsonpatch of modified object %s/%s", m.After.GetNamespace(), m.After.GetName()) - } - patchFileName := fmt.Sprintf("%s_%s_%s.jsonpatch", m.After.GetKind(), m.After.GetNamespace(), m.After.GetName()) - patchFilePath := path.Join(modifiedDir, patchFileName) - if err := os.WriteFile(patchFilePath, jsonPatch, 0600); err != nil { - return pkgerrors.Wrapf(err, "failed to write jsonpatch to file %q", patchFilePath) - } - - // Calculate the diff and write to a file. - diffFileName := fmt.Sprintf("%s_%s_%s.diff", m.After.GetKind(), m.After.GetNamespace(), m.After.GetName()) - diffFilePath := path.Join(modifiedDir, diffFileName) - diffFile, err := os.OpenFile(filepath.Clean(diffFilePath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return pkgerrors.Wrapf(err, "unable to open file %q", diffFilePath) - } - if err := writeDiffToFile(filePathOriginal, filePathModified, diffFile); err != nil { - return pkgerrors.Wrapf(err, "failed to write diff to file %q", diffFilePath) - } - } - if len(out.Modified) != 0 { - fmt.Printf("Modified objects are written to directory %q\n", modifiedDir) - } - - return nil -} - -func writeObjectToFile(filePath string, obj *unstructured.Unstructured) error { - yaml, err := utilyaml.FromUnstructured([]unstructured.Unstructured{*obj}) - if err != nil { - return pkgerrors.Wrap(err, "failed to convert object to yaml") - } - if err := os.WriteFile(filePath, yaml, 0600); err != nil { - return pkgerrors.Wrapf(err, "failed to write yaml to file %q", filePath) - } - return nil -} - -func convertToPtrSlice(objs []unstructured.Unstructured) []*unstructured.Unstructured { - res := []*unstructured.Unstructured{} - for i := range objs { - res = append(res, &objs[i]) - } - return res -} - -func lessByKindAndName(a, b *unstructured.Unstructured) bool { - if a.GetKind() == b.GetKind() { - return a.GetName() < b.GetName() - } - return a.GetKind() < b.GetKind() -} - -func addRow(table *tablewriter.Table, o *unstructured.Unstructured, action string, actionColor int) { - table.Rich( - []string{ - o.GetNamespace(), - o.GetKind(), - o.GetName(), - action, - }, - []tablewriter.Colors{ - {}, {}, {}, {actionColor}, - }, - ) -} - -// writeDiffToFile runs the detected diff program. `from` and `to` are the files to diff. -// The implementation is highly inspired by kubectl's DiffProgram implementation: -// ref: https://github.com/kubernetes/kubectl/blob/v0.24.3/pkg/cmd/diff/diff.go#L218 -func writeDiffToFile(from, to string, out io.Writer) error { - diff, cmd := getDiffCommand(from, to) - cmd.SetStdout(out) - - if err := cmd.Run(); err != nil && !isDiffError(err) { - return pkgerrors.Wrapf(err, "failed to run %q", diff) - } - return nil -} - -func getDiffCommand(args ...string) (string, exec.Cmd) { - diff := "" - if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" { - diffCommand := strings.Split(envDiff, " ") - diff = diffCommand[0] - - if len(diffCommand) > 1 { - // Regex accepts: Alphanumeric (case-insensitive), dash and equal - isValidChar := regexp.MustCompile(`^[a-zA-Z0-9-=]+$`).MatchString - for i := 1; i < len(diffCommand); i++ { - if isValidChar(diffCommand[i]) { - args = append(args, diffCommand[i]) - } - } - } - } else { - diff = "diff" - args = append([]string{"-u", "-N"}, args...) - } - - cmd := exec.New().Command(diff, args...) - - return diff, cmd -} - -// diffError returns true if the status code is lower or equal to 1, false otherwise. -// This makes use of the exit code of diff programs which is 0 for no diff, 1 for -// modified and 2 for other errors. -func isDiffError(err error) bool { - var exitErr exec.ExitError - if errors.As(err, &exitErr) && exitErr.ExitStatus() <= 1 { - return true - } - return false -} diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index ab4a26f80d5f..d2e7feb3f3a7 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -60,7 +60,6 @@ - [delete](clusterctl/commands/delete.md) - [completion](clusterctl/commands/completion.md) - [alpha rollout](clusterctl/commands/alpha-rollout.md) - - [alpha topology plan](clusterctl/commands/alpha-topology-plan.md) - [additional commands](clusterctl/commands/additional-commands.md) - [clusterctl Configuration](clusterctl/configuration.md) - [clusterctl for Developers](clusterctl/developers.md) diff --git a/docs/book/src/clusterctl/commands/alpha-topology-plan.md b/docs/book/src/clusterctl/commands/alpha-topology-plan.md deleted file mode 100644 index 0f7d3ba4ce7d..000000000000 --- a/docs/book/src/clusterctl/commands/alpha-topology-plan.md +++ /dev/null @@ -1,482 +0,0 @@ -# clusterctl alpha topology plan - - - -The `clusterctl alpha topology plan` command can be used to get a plan of how a Cluster topology evolves given -file(s) containing resources to be applied to a Cluster. - -The input file(s) could contain a new/modified Cluster, a new/modified ClusterClass and/or new/modified templates, -depending on the use case you are going to plan for (see more details below). - -The topology plan output would provide details about objects that will be created, updated and deleted of a target cluster; -If instead the command detects that the change impacts many Clusters, the users will be required to select one to focus on (see flags below). - -```bash -clusterctl alpha topology plan -f input.yaml -o output/ -``` - - - - - - - -## Example use cases - -### Designing a new ClusterClass - -When designing a new ClusterClass users might want to preview the Cluster generated using such ClusterClass. -The `clusterctl alpha topology plan command` can be used to do so: - -```bash -clusterctl alpha topology plan -f example-cluster-class.yaml -f example-cluster.yaml -o output/ -``` - -`example-cluster-class.yaml` holds the definitions of the ClusterClass and all the associated templates. -
-View example-cluster-class.yaml - -```yaml -apiVersion: cluster.x-k8s.io/v1beta1 -kind: ClusterClass -metadata: - name: example-cluster-class - namespace: default -spec: - controlPlane: - ref: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 - kind: KubeadmControlPlaneTemplate - name: example-cluster-control-plane - namespace: default - machineInfrastructure: - ref: - kind: DockerMachineTemplate - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - name: "example-cluster-control-plane" - namespace: default - infrastructure: - ref: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: DockerClusterTemplate - name: example-cluster - namespace: default - workers: - machineDeployments: - - class: "default-worker" - template: - bootstrap: - ref: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 - kind: KubeadmConfigTemplate - name: example-docker-worker-bootstraptemplate - infrastructure: - ref: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: DockerMachineTemplate - name: example-docker-worker-machinetemplate ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 -kind: DockerClusterTemplate -metadata: - name: example-cluster - namespace: default -spec: - template: - spec: {} ---- -kind: KubeadmControlPlaneTemplate -apiVersion: controlplane.cluster.x-k8s.io/v1beta1 -metadata: - name: "example-cluster-control-plane" - namespace: default -spec: - template: - spec: - machineTemplate: - nodeDrainTimeout: 1s - kubeadmConfigSpec: - clusterConfiguration: - apiServer: - certSANs: [ localhost, 127.0.0.1 ] - initConfiguration: - nodeRegistration: {} # node registration parameters are automatically injected by CAPD according to the kindest/node image in use. - joinConfiguration: - nodeRegistration: {} # node registration parameters are automatically injected by CAPD according to the kindest/node image in use. ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 -kind: DockerMachineTemplate -metadata: - name: "example-cluster-control-plane" - namespace: default -spec: - template: - spec: - extraMounts: - - containerPath: "/var/run/docker.sock" - hostPath: "/var/run/docker.sock" ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 -kind: DockerMachineTemplate -metadata: - name: "example-docker-worker-machinetemplate" - namespace: default -spec: - template: - spec: {} ---- -apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 -kind: KubeadmConfigTemplate -metadata: - name: "example-docker-worker-bootstraptemplate" - namespace: default -spec: - template: - spec: - joinConfiguration: - nodeRegistration: {} # node registration parameters are automatically injected by CAPD according to the kindest/node image in use. -``` - -
- -`example-cluster.yaml` holds the definition of `example-cluster` Cluster. -
-View example-cluster.yaml - -```yaml -apiVersion: cluster.x-k8s.io/v1beta1 -kind: Cluster -metadata: - name: "example-cluster" - namespace: "default" - labels: - cni: kindnet -spec: - clusterNetwork: - services: - cidrBlocks: ["10.128.0.0/12"] - pods: - cidrBlocks: ["192.168.0.0/16"] - serviceDomain: "cluster.local" - topology: - class: example-cluster-class - version: v1.21.2 - controlPlane: - metadata: {} - replicas: 1 - workers: - machineDeployments: - - class: "default-worker" - name: "md-0" - replicas: 1 -``` - -
- -Produces an output similar to this: -```bash -The following ClusterClasses will be affected by the changes: - * default/example-cluster-class - -The following Clusters will be affected by the changes: - * default/example-cluster - -Changes for Cluster "default/example-cluster": - - NAMESPACE KIND NAME ACTION - default DockerCluster example-cluster-rnx2q created - default DockerMachineTemplate example-cluster-control-plane-dfnvz created - default DockerMachineTemplate example-cluster-md-0-infra-qz9qk created - default KubeadmConfigTemplate example-cluster-md-0-bootstrap-m29vz created - default KubeadmControlPlane example-cluster-b2lhc created - default MachineDeployment example-cluster-md-0-pqscg created - default Secret example-cluster-shim created - default Cluster example-cluster modified - -Created objects are written to directory "output/created" -Modified objects are written to directory "output/modified" -``` - -The contents of the output directory are similar to this: -```bash -output -├── created -│ ├── DockerCluster_default_example-cluster-rnx2q.yaml -│ ├── DockerMachineTemplate_default_example-cluster-control-plane-dfnvz.yaml -│ ├── DockerMachineTemplate_default_example-cluster-md-0-infra-qz9qk.yaml -│ ├── KubeadmConfigTemplate_default_example-cluster-md-0-bootstrap-m29vz.yaml -│ ├── KubeadmControlPlane_default_example-cluster-b2lhc.yaml -│ ├── MachineDeployment_default_example-cluster-md-0-pqscg.yaml -│ └── Secret_default_example-cluster-shim.yaml -└── modified - ├── Cluster_default_example-cluster.diff - ├── Cluster_default_example-cluster.jsonpatch - ├── Cluster_default_example-cluster.modified.yaml - └── Cluster_default_example-cluster.original.yaml -``` - -### Plan changes to Cluster topology - -When making changes to a Cluster topology the `clusterctl alpha topology plan` can be used to analyse how the underlying objects will be affected. - -```bash -clusterctl alpha topology plan -f modified-example-cluster.yaml -o output/ -``` - -The `modified-example-cluster.yaml` scales up the control plane to 3 replicas and adds additional labels to the machine deployment. -
-View modified-example-cluster.yaml - -```yaml -apiVersion: cluster.x-k8s.io/v1beta1 -kind: Cluster -metadata: - name: "example-cluster" - namespace: default - labels: - cni: kindnet -spec: - clusterNetwork: - services: - cidrBlocks: ["10.128.0.0/12"] - pods: - cidrBlocks: ["192.168.0.0/16"] - serviceDomain: "cluster.local" - topology: - class: example-cluster-class - version: v1.21.2 - controlPlane: - metadata: {} - # Scale up the control plane from 1 -> 3. - replicas: 3 - workers: - machineDeployments: - - class: "default-worker" - # Apply additional labels. - metadata: - labels: - test-label: md-0-label - name: "md-0" - replicas: 1 -``` -
- -Produces an output similar to this: -```bash -Detected a cluster with Cluster API installed. Will use it to fetch missing objects. -No ClusterClasses will be affected by the changes. -The following Clusters will be affected by the changes: - * default/example-cluster - -Changes for Cluster "default/example-cluster": - - NAMESPACE KIND NAME ACTION - default KubeadmControlPlane example-cluster-l7kx8 modified - default MachineDeployment example-cluster-md-0-j58ln modified - -Modified objects are written to directory "output/modified" -``` - -### Rebase a Cluster to a different ClusterClass - -The command can be used to plan if a Cluster can be successfully rebased to a different ClusterClass. - -Rebasing a Cluster to a different ClusterClass: -```bash -# Rebasing from `example-cluster-class` to `another-cluster-class`. -clusterctl alpha topology plan -f rebase-example-cluster.yaml -o output/ -``` -The `example-cluster` Cluster is rebased from `example-cluster-class` to `another-cluster-class`. In this example `another-cluster-class` is assumed to be available in the management cluster. - -
-View rebase-example-cluster.yaml - -```yaml -apiVersion: cluster.x-k8s.io/v1beta1 -kind: Cluster -metadata: - name: "example-cluster" - namespace: "default" - labels: - cni: kindnet -spec: - clusterNetwork: - services: - cidrBlocks: ["10.128.0.0/12"] - pods: - cidrBlocks: ["192.168.0.0/16"] - serviceDomain: "cluster.local" - topology: - # ClusterClass changed from 'example-cluster-class' -> 'another-cluster-class'. - class: another-cluster-class - version: v1.21.2 - controlPlane: - metadata: {} - replicas: 1 - workers: - machineDeployments: - - class: "default-worker" - name: "md-0" - replicas: 1 -``` -
- -If the target ClusterClass is compatible with the original ClusterClass the output be similar to: -```bash -Detected a cluster with Cluster API installed. Will use it to fetch missing objects. -No ClusterClasses will be affected by the changes. -The following Clusters will be affected by the changes: - * default/example-cluster - -Changes for Cluster "default/example-cluster": - - NAMESPACE KIND NAME ACTION - default DockerCluster example-cluster-7t7pl modified - default DockerMachineTemplate example-cluster-control-plane-lt6kw modified - default DockerMachineTemplate example-cluster-md-0-infra-cjxs4 modified - default KubeadmConfigTemplate example-cluster-md-0-bootstrap-m9sg8 modified - default KubeadmControlPlane example-cluster-l7kx8 modified - -Modified objects are written to directory "output/modified" -``` - -Instead, if the command detects that the rebase operation would lead to a non-functional cluster (ClusterClasses are incompatible), the output will be similar to: -```bash -Detected a cluster with Cluster API installed. Will use it to fetch missing objects. -Error: failed defaulting and validation on input objects: failed to run defaulting and validation on Clusters: failed validation of cluster.x-k8s.io/v1beta1, Kind=Cluster default/example-cluster: Cluster.cluster.x-k8s.io "example-cluster" is invalid: spec.topology.workers.machineDeployments[0].class: Invalid value: "default-worker": MachineDeploymentClass with name "default-worker" does not exist in ClusterClass "another-cluster-class" -``` -In this example rebasing will lead to a non-functional Cluster because the ClusterClass is missing a worker class that is used by the Cluster. - -### Testing the effects of changing a ClusterClass - -When planning for a change on a ClusterClass you might want to understand what effects the change will have on existing clusters. - -```bash -clusterctl alpha topology plan -f modified-first-cluster-class.yaml -o output/ -``` -When multiple clusters are affected, only the list of Clusters and ClusterClasses is presented. -```bash -Detected a cluster with Cluster API installed. Will use it to fetch missing objects. -The following ClusterClasses will be affected by the changes: - * default/first-cluster-class - -The following Clusters will be affected by the changes: - * default/first-cluster - * default/second-cluster - -No target cluster identified. Use --cluster to specify a target cluster to get detailed changes. -``` - -To get the full list of changes for the "first-cluster": -```bash -clusterctl alpha topology plan -f modified-first-cluster-class.yaml -o output/ -c "first-cluster" -``` -Output will be similar to the full summary output provided in other examples. - -## How does `topology plan` work? - -The topology plan operation is composed of the following steps: -* Set the namespace on objects in the input with missing namespace. -* Run the Defaulting and Validation webhooks on the Cluster and ClusterClass objects in the input. -* Dry run the topology reconciler on the target cluster. -* Capture all changes observed during reconciliation. - -## Reference - -### `--file`, `-f` (REQUIRED) - -The input file(s) with the target changes. Supports multiple input files. - -The objects in the input should follow these rules: -* All the objects in the input should belong to the same namespace. -* Should not have multiple Clusters. -* Should not have multiple ClusterClasses. - - - - - - - -### `--output-directory`, `-o` (REQUIRED) - -Information about the objects that are created and updated is written to this directory. - -For objects that are modified the following files are written to disk: -* Original object -* Final object -* JSON patch between the original and the final objects -* Diff of the original and final objects - -### `--cluster`, `-c` (Optional) - -When multiple clusters are affected by the input, `--cluster` can be used to specify a target cluster. - -If only one cluster is affected or if a Cluster is in the input it defaults as the target cluster. - -### `--namespace`, `-n` (Optional) - -Namespace used for objects with missing namespaces in the input. - -If not provided, the namespace defined in kubeconfig is used. If a kubeconfig is not available the value `default` is used. diff --git a/docs/book/src/clusterctl/commands/commands.md b/docs/book/src/clusterctl/commands/commands.md index c5b353155a49..8cabf64b2927 100644 --- a/docs/book/src/clusterctl/commands/commands.md +++ b/docs/book/src/clusterctl/commands/commands.md @@ -3,7 +3,6 @@ | Command | Description | |------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| | [`clusterctl alpha rollout`](alpha-rollout.md) | Manages the rollout of Cluster API resources. For example: MachineDeployments. | -| [`clusterctl alpha topology plan`](alpha-topology-plan.md) | Describes the changes to a cluster topology for a given input. | | [`clusterctl completion`](completion.md) | Output shell completion code for the specified shell (bash or zsh). | | [`clusterctl config`](additional-commands.md#clusterctl-config-repositories) | Display clusterctl configuration. | | [`clusterctl delete`](delete.md) | Delete one or more providers from the management cluster. | diff --git a/docs/book/src/tasks/experimental-features/cluster-class/change-clusterclass.md b/docs/book/src/tasks/experimental-features/cluster-class/change-clusterclass.md index 7bf08c02d49f..b608d14e023e 100644 --- a/docs/book/src/tasks/experimental-features/cluster-class/change-clusterclass.md +++ b/docs/book/src/tasks/experimental-features/cluster-class/change-clusterclass.md @@ -119,14 +119,7 @@ defined in the ClusterClass proposal. ## Planning ClusterClass changes -It is highly recommended to always generate a plan for ClusterClass changes before applying them, -no matter if you are creating a new ClusterClass and rebasing Clusters or if you are changing -your ClusterClass in place. - -The clusterctl tool provides a new alpha command for this operation, [clusterctl alpha topology plan](../../../clusterctl/commands/alpha-topology-plan.md). - -The output of this command will provide you all the details about how those changes would impact -Clusters, but the following notes can help you to understand what you should +Some general notes that can help you to understand what you should expect when planning your ClusterClass changes: - Users should expect the resources in a Cluster (e.g. MachineDeployments) to behave consistently @@ -137,7 +130,7 @@ expect when planning your ClusterClass changes: - User should expect the Cluster topology to change consistently irrespective of how the change has been implemented inside the ClusterClass or applied to the ClusterClass. In other words, - if you change a template field "in place", or if you rotate the template referenced in the + if you change a template field "in place", or if you rotate the template referenced in the ClusterClass by pointing to a new template with the same field changed, or if you change the same field via a patch, the effects on the Cluster are the same. @@ -192,4 +185,4 @@ set of fields that are enforced should be determined by applying patches on top A corollary of the behaviour described above is that it is technically possible to change fields in the object which are not derived from the templates and patches, but we advise against using the possibility or making ad-hoc changes in generated objects unless otherwise needed for a workaround. It is always -preferable to improve ClusterClasses by supporting new Cluster variants in a reusable way. \ No newline at end of file +preferable to improve ClusterClasses by supporting new Cluster variants in a reusable way. diff --git a/docs/book/src/tasks/experimental-features/cluster-class/index.md b/docs/book/src/tasks/experimental-features/cluster-class/index.md index 31476cfb4a2c..8dd900fa1e48 100644 --- a/docs/book/src/tasks/experimental-features/cluster-class/index.md +++ b/docs/book/src/tasks/experimental-features/cluster-class/index.md @@ -23,9 +23,7 @@ Additional documentation: * Creating a Cluster: [Quick Start guide] Please note that the experience for creating a Cluster using ClusterClass is very similar to the one for creating a standalone Cluster. Infrastructure providers supporting ClusterClass provide Cluster templates leveraging this feature (e.g the Docker infrastructure provider has a development-topology template). * [Operating a managed Cluster](./operate-cluster.md) - * Planning topology rollouts: [clusterctl alpha topology plan] [Quick Start guide]: ../../../user/quick-start.md [clusterctl Provider contract]: ../../../developer/providers/contracts/clusterctl.md -[clusterctl alpha topology plan]: ../../../clusterctl/commands/alpha-topology-plan.md diff --git a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md index 6e72a0d314cf..2961b0794cb8 100644 --- a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md +++ b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md @@ -128,16 +128,6 @@ For a full example ClusterClass for CAPD you can take a look at - - ## ClusterClass with MachinePools ClusterClass also supports MachinePool workers. They work very similar to MachineDeployments. MachinePools @@ -1065,5 +1055,4 @@ spec: [Changing a ClusterClass]: ./change-clusterclass.md -[clusterctl alpha topology plan]: ../../../clusterctl/commands/alpha-topology-plan.md [RFC6902]: https://datatracker.ietf.org/doc/html/rfc6902#appendix-A.12 diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 4caeab34d17c..dd71bed6ce6e 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -41,8 +41,6 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect @@ -50,24 +48,17 @@ require ( github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/btree v1.1.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/spf13/cobra v1.9.1 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/randfill v1.0.0 // indirect ) @@ -77,9 +68,6 @@ require ( cloud.google.com/go/iam v1.5.2 // indirect dario.cat/mergo v1.0.1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect @@ -113,7 +101,6 @@ require ( github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/cel-go v0.23.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-github/v58 v58.0.0 // indirect @@ -122,7 +109,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/huandu/xstrings v1.5.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -131,8 +117,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -149,14 +133,12 @@ require ( github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/saschagrunert/go-modiff v1.3.5 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/viper v1.20.1 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect @@ -166,7 +148,6 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.38.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sync v0.14.0 // indirect diff --git a/hack/tools/go.sum b/hack/tools/go.sum index d2091db9ee5f..11fefa9090b0 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -77,12 +77,6 @@ github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= github.com/coredns/corefile-migration v1.0.26 h1:xiiEkVB1Dwolb24pkeDUDBfygV9/XsOSq79yFCrhptY= github.com/coredns/corefile-migration v1.0.26/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -204,8 +198,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -302,7 +295,6 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/saschagrunert/go-modiff v1.3.5 h1:Wb2KUhCiuTJfhCwGYIwjZOpC++RbY0MTf7J5m1CfQlw= @@ -359,12 +351,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= -go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= -go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= -go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= -go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= -go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= -go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= diff --git a/internal/controllers/topology/cluster/blueprint_test.go b/internal/controllers/topology/cluster/blueprint_test.go index 8b0f650e30bb..5ac95e594d80 100644 --- a/internal/controllers/topology/cluster/blueprint_test.go +++ b/internal/controllers/topology/cluster/blueprint_test.go @@ -393,8 +393,7 @@ func TestGetBlueprint(t *testing.T) { // Calls getBlueprint. r := &Reconciler{ - Client: fakeClient, - patchHelperFactory: dryRunPatchHelperFactory(fakeClient), + Client: fakeClient, } got, err := r.getBlueprint(ctx, scope.New(cluster).Current.Cluster, tt.clusterClass) diff --git a/internal/controllers/topology/cluster/cluster_controller.go b/internal/controllers/topology/cluster/cluster_controller.go index aef1b601826a..83ee336c5c03 100644 --- a/internal/controllers/topology/cluster/cluster_controller.go +++ b/internal/controllers/topology/cluster/cluster_controller.go @@ -30,16 +30,13 @@ import ( kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" - "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/cache/informertest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -49,13 +46,11 @@ import ( runtimehooksv1 "sigs.k8s.io/cluster-api/api/runtime/hooks/v1alpha1" "sigs.k8s.io/cluster-api/controllers/clustercache" "sigs.k8s.io/cluster-api/controllers/external" - externalfake "sigs.k8s.io/cluster-api/controllers/external/fake" runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog" runtimeclient "sigs.k8s.io/cluster-api/exp/runtime/client" "sigs.k8s.io/cluster-api/exp/topology/desiredstate" "sigs.k8s.io/cluster-api/exp/topology/scope" "sigs.k8s.io/cluster-api/feature" - "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/structuredmerge" "sigs.k8s.io/cluster-api/internal/hooks" "sigs.k8s.io/cluster-api/internal/util/ssa" "sigs.k8s.io/cluster-api/internal/webhooks" @@ -94,7 +89,7 @@ type Reconciler struct { // desiredStateGenerator is used to generate the desired state. desiredStateGenerator desiredstate.Generator - patchHelperFactory structuredmerge.PatchHelperFactoryFunc + ssaCache ssa.Cache } func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { @@ -159,9 +154,7 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt } r.desiredStateGenerator = desiredstate.NewGenerator(r.Client, r.ClusterCache, r.RuntimeClient) r.recorder = mgr.GetEventRecorderFor("topology/cluster-controller") - if r.patchHelperFactory == nil { - r.patchHelperFactory = serverSideApplyPatchHelperFactory(r.Client, ssa.NewCache("topology/cluster")) - } + r.ssaCache = ssa.NewCache("topology/cluster") return nil } @@ -256,19 +249,6 @@ func machineDeploymentChangeIsRelevant(scheme *runtime.Scheme, logger logr.Logge } } -// SetupForDryRun prepares the Reconciler for a dry run execution. -func (r *Reconciler) SetupForDryRun(recorder record.EventRecorder) { - r.desiredStateGenerator = desiredstate.NewGenerator(r.Client, r.ClusterCache, r.RuntimeClient) - r.recorder = recorder - r.externalTracker = external.ObjectTracker{ - Controller: externalfake.Controller{}, - Cache: &informertest.FakeInformers{}, - Scheme: r.Client.Scheme(), - PredicateLogger: ptr.To(logr.New(log.NullLogSink{})), - } - r.patchHelperFactory = dryRunPatchHelperFactory(r.Client) -} - func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { // Fetch the Cluster instance. cluster := &clusterv1.Cluster{} @@ -565,17 +545,3 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, cluster *clusterv1.Clu } return ctrl.Result{}, nil } - -// serverSideApplyPatchHelperFactory makes use of managed fields provided by server side apply and is used by the controller. -func serverSideApplyPatchHelperFactory(c client.Client, ssaCache ssa.Cache) structuredmerge.PatchHelperFactoryFunc { - return func(ctx context.Context, original, modified client.Object, opts ...structuredmerge.HelperOption) (structuredmerge.PatchHelper, error) { - return structuredmerge.NewServerSidePatchHelper(ctx, original, modified, c, ssaCache, opts...) - } -} - -// dryRunPatchHelperFactory makes use of a two-ways patch and is used in situations where we cannot rely on managed fields. -func dryRunPatchHelperFactory(c client.Client) structuredmerge.PatchHelperFactoryFunc { - return func(_ context.Context, original, modified client.Object, opts ...structuredmerge.HelperOption) (structuredmerge.PatchHelper, error) { - return structuredmerge.NewTwoWaysPatchHelper(original, modified, c, opts...) - } -} diff --git a/internal/controllers/topology/cluster/current_state_test.go b/internal/controllers/topology/cluster/current_state_test.go index eea71a182e3c..9e9e8e75e974 100644 --- a/internal/controllers/topology/cluster/current_state_test.go +++ b/internal/controllers/topology/cluster/current_state_test.go @@ -1150,9 +1150,8 @@ func TestGetCurrentState(t *testing.T) { // Calls getCurrentState. r := &Reconciler{ - Client: fakeClient, - APIReader: fakeClient, - patchHelperFactory: dryRunPatchHelperFactory(fakeClient), + Client: fakeClient, + APIReader: fakeClient, } got, err := r.getCurrentState(ctx, s) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index 9300e86d1ce8..09a506656b27 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -396,7 +396,7 @@ func (r *Reconciler) reconcileMachineHealthCheck(ctx context.Context, current, d // If a current MachineHealthCheck doesn't exist but there is a desired MachineHealthCheck attempt to create. if current == nil && desired != nil { log.Info("Creating MachineHealthCheck", "MachineHealthCheck", klog.KObj(desired)) - helper, err := r.patchHelperFactory(ctx, nil, desired) + helper, err := structuredmerge.NewServerSidePatchHelper(ctx, nil, desired, r.Client, r.ssaCache) if err != nil { return errors.Wrapf(err, "failed to create patch helper for MachineHealthCheck %s", klog.KObj(desired)) } @@ -426,7 +426,7 @@ func (r *Reconciler) reconcileMachineHealthCheck(ctx context.Context, current, d // Check differences between current and desired MachineHealthChecks, and patch if required. // NOTE: we want to be authoritative on the entire spec because the users are // expected to change MHC fields from the ClusterClass only. - patchHelper, err := r.patchHelperFactory(ctx, current, desired) + patchHelper, err := structuredmerge.NewServerSidePatchHelper(ctx, current, desired, r.Client, r.ssaCache) if err != nil { return errors.Wrapf(err, "failed to create patch helper for MachineHealthCheck %s", klog.KObj(current)) } @@ -451,7 +451,7 @@ func (r *Reconciler) reconcileCluster(ctx context.Context, s *scope.Scope) error log := ctrl.LoggerFrom(ctx) // Check differences between current and desired state, and eventually patch the current object. - patchHelper, err := r.patchHelperFactory(ctx, s.Current.Cluster, s.Desired.Cluster) + patchHelper, err := structuredmerge.NewServerSidePatchHelper(ctx, s.Current.Cluster, s.Desired.Cluster, r.Client, r.ssaCache) if err != nil { return errors.Wrapf(err, "failed to create patch helper for Cluster %s", klog.KObj(s.Current.Cluster)) } @@ -633,7 +633,7 @@ func (r *Reconciler) createMachineDeployment(ctx context.Context, s *scope.Scope } log.Info("Creating MachineDeployment") - helper, err := r.patchHelperFactory(ctx, nil, md.Object) + helper, err := structuredmerge.NewServerSidePatchHelper(ctx, nil, md.Object, r.Client, r.ssaCache) if err != nil { // Best effort cleanup of the InfrastructureMachineTemplate & BootstrapTemplate (only on creation). infrastructureMachineCleanupFunc() @@ -753,7 +753,7 @@ func (r *Reconciler) updateMachineDeployment(ctx context.Context, s *scope.Scope } // Check differences between current and desired MachineDeployment, and eventually patch the current object. - patchHelper, err := r.patchHelperFactory(ctx, currentMD.Object, desiredMD.Object) + patchHelper, err := structuredmerge.NewServerSidePatchHelper(ctx, currentMD.Object, desiredMD.Object, r.Client, r.ssaCache) if err != nil { // Best effort cleanup of the InfrastructureMachineTemplate & BootstrapTemplate (only on template rotation). infrastructureMachineCleanupFunc() @@ -975,7 +975,7 @@ func (r *Reconciler) createMachinePool(ctx context.Context, s *scope.Scope, mp * } log.Info("Creating MachinePool") - helper, err := r.patchHelperFactory(ctx, nil, mp.Object) + helper, err := structuredmerge.NewServerSidePatchHelper(ctx, nil, mp.Object, r.Client, r.ssaCache) if err != nil { // Best effort cleanup of the InfrastructureMachinePool & BootstrapConfig (only on creation). infrastructureMachineMachinePoolCleanupFunc() @@ -1042,7 +1042,7 @@ func (r *Reconciler) updateMachinePool(ctx context.Context, s *scope.Scope, mpTo } // Check differences between current and desired MachinePool, and eventually patch the current object. - patchHelper, err := r.patchHelperFactory(ctx, currentMP.Object, desiredMP.Object) + patchHelper, err := structuredmerge.NewServerSidePatchHelper(ctx, currentMP.Object, desiredMP.Object, r.Client, r.ssaCache) if err != nil { return errors.Wrapf(err, "failed to create patch helper for MachinePool %s", klog.KObj(currentMP.Object)) } @@ -1177,7 +1177,7 @@ func (r *Reconciler) reconcileReferencedObject(ctx context.Context, in reconcile // If there is no current object, create it. if in.current == nil { log.Info(fmt.Sprintf("Creating %s", in.desired.GetKind()), in.desired.GetKind(), klog.KObj(in.desired)) - helper, err := r.patchHelperFactory(ctx, nil, in.desired, structuredmerge.IgnorePaths(in.ignorePaths)) + helper, err := structuredmerge.NewServerSidePatchHelper(ctx, nil, in.desired, r.Client, r.ssaCache, structuredmerge.IgnorePaths(in.ignorePaths)) if err != nil { return false, errors.Wrap(createErrorWithoutObjectName(ctx, err, in.desired), "failed to create patch helper") } @@ -1197,7 +1197,7 @@ func (r *Reconciler) reconcileReferencedObject(ctx context.Context, in reconcile ctx = ctrl.LoggerInto(ctx, log) // Check differences between current and desired state, and eventually patch the current object. - patchHelper, err := r.patchHelperFactory(ctx, in.current, in.desired, structuredmerge.IgnorePaths(in.ignorePaths)) + patchHelper, err := structuredmerge.NewServerSidePatchHelper(ctx, in.current, in.desired, r.Client, r.ssaCache, structuredmerge.IgnorePaths(in.ignorePaths)) if err != nil { return false, errors.Wrapf(err, "failed to create patch helper for %s %s", in.current.GetKind(), klog.KObj(in.current)) } @@ -1263,7 +1263,7 @@ func (r *Reconciler) reconcileReferencedTemplate(ctx context.Context, in reconci // If there is no current object, create the desired object. if in.current == nil { log.Info(fmt.Sprintf("Creating %s", in.desired.GetKind()), in.desired.GetKind(), klog.KObj(in.desired)) - helper, err := r.patchHelperFactory(ctx, nil, in.desired) + helper, err := structuredmerge.NewServerSidePatchHelper(ctx, nil, in.desired, r.Client, r.ssaCache) if err != nil { return false, errors.Wrap(createErrorWithoutObjectName(ctx, err, in.desired), "failed to create patch helper") } @@ -1287,7 +1287,7 @@ func (r *Reconciler) reconcileReferencedTemplate(ctx context.Context, in reconci ctx = ctrl.LoggerInto(ctx, log) // Check differences between current and desired objects, and if there are changes eventually start the template rotation. - patchHelper, err := r.patchHelperFactory(ctx, in.current, in.desired) + patchHelper, err := structuredmerge.NewServerSidePatchHelper(ctx, in.current, in.desired, r.Client, r.ssaCache) if err != nil { return false, errors.Wrapf(err, "failed to create patch helper for %s %s", in.current.GetKind(), klog.KObj(in.current)) } @@ -1328,7 +1328,7 @@ func (r *Reconciler) reconcileReferencedTemplate(ctx context.Context, in reconci log.Info(fmt.Sprintf("Rotating %s, new name %s", in.current.GetKind(), newName), "diff", string(changes)) } log.Info(fmt.Sprintf("Creating %s", in.current.GetKind())) - helper, err := r.patchHelperFactory(ctx, nil, in.desired) + helper, err := structuredmerge.NewServerSidePatchHelper(ctx, nil, in.desired, r.Client, r.ssaCache) if err != nil { return false, errors.Wrap(createErrorWithoutObjectName(ctx, err, in.desired), "failed to create patch helper") } diff --git a/internal/controllers/topology/cluster/reconcile_state_test.go b/internal/controllers/topology/cluster/reconcile_state_test.go index 00ce08d65354..64f06e420d86 100644 --- a/internal/controllers/topology/cluster/reconcile_state_test.go +++ b/internal/controllers/topology/cluster/reconcile_state_test.go @@ -62,8 +62,6 @@ var IgnoreNameGenerated = IgnorePaths{ "metadata.name", } -const testController = "test-controller" - func TestReconcileShim(t *testing.T) { infrastructureCluster := builder.TestInfrastructureCluster(metav1.NamespaceDefault, "infrastructure-cluster1").Build() controlPlane := builder.TestControlPlane(metav1.NamespaceDefault, "controlplane-cluster1").Build() @@ -94,9 +92,8 @@ func TestReconcileShim(t *testing.T) { // Run reconcileClusterShim. r := Reconciler{ - Client: env, - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), + Client: env, + APIReader: env.GetAPIReader(), } err = r.reconcileClusterShim(ctx, s) g.Expect(err).ToNot(HaveOccurred()) @@ -137,9 +134,8 @@ func TestReconcileShim(t *testing.T) { // Run reconcileClusterShim. r := Reconciler{ - Client: env, - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), + Client: env, + APIReader: env.GetAPIReader(), } err = r.reconcileClusterShim(ctx, s) g.Expect(err).ToNot(HaveOccurred()) @@ -187,9 +183,8 @@ func TestReconcileShim(t *testing.T) { // Run reconcileClusterShim. r := Reconciler{ - Client: env, - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), + Client: env, + APIReader: env.GetAPIReader(), } err = r.reconcileClusterShim(ctx, s) g.Expect(err).ToNot(HaveOccurred()) @@ -238,9 +233,8 @@ func TestReconcileShim(t *testing.T) { // Run reconcileClusterShim. r := Reconciler{ - Client: env, - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), + Client: env, + APIReader: env.GetAPIReader(), } err = r.reconcileClusterShim(ctx, s) g.Expect(err).ToNot(HaveOccurred()) @@ -279,9 +273,8 @@ func TestReconcileShim(t *testing.T) { // Run reconcileClusterShim using a nil client, so an error will be triggered if any operation is attempted r := Reconciler{ - Client: nil, - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(nil, ssa.NewCache(testController)), + Client: nil, + APIReader: env.GetAPIReader(), } err = r.reconcileClusterShim(ctx, s) g.Expect(err).ToNot(HaveOccurred()) @@ -1155,9 +1148,9 @@ func TestReconcileCluster(t *testing.T) { s.Desired = &scope.ClusterState{Cluster: tt.desired} r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileCluster(ctx, s) if tt.wantErr { @@ -1282,9 +1275,9 @@ func TestReconcileInfrastructureCluster(t *testing.T) { s.Desired = &scope.ClusterState{InfrastructureCluster: tt.desired.DeepCopy()} r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } created, err := r.reconcileInfrastructureCluster(ctx, s) if tt.wantErr { @@ -1556,9 +1549,9 @@ func TestReconcileControlPlane(t *testing.T) { } r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } s.Desired = &scope.ClusterState{ @@ -1712,9 +1705,8 @@ func TestReconcileControlPlaneCleanup(t *testing.T) { s.Desired.ControlPlane.Object.SetNamespace("do-not-exist") r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), } created, err := r.reconcileControlPlane(ctx, s) g.Expect(err).To(HaveOccurred()) @@ -1877,9 +1869,9 @@ func TestReconcileControlPlaneMachineHealthCheck(t *testing.T) { } r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } s.Desired = &scope.ClusterState{ @@ -2175,10 +2167,10 @@ func TestReconcileMachineDeployments(t *testing.T) { } r := Reconciler{ - Client: env.GetClient(), - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env.GetClient(), + APIReader: env.GetAPIReader(), + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileMachineDeployments(ctx, s) if tt.wantErr { @@ -2293,10 +2285,9 @@ func TestReconcileMachineDeploymentsCleanup(t *testing.T) { s.Desired.MachineDeployments[md1.Object.Name].Object.Namespace = "do-not-exist" r := Reconciler{ - Client: env.GetClient(), - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env.GetClient(), + APIReader: env.GetAPIReader(), + recorder: env.GetEventRecorderFor("test"), } err = r.reconcileMachineDeployments(ctx, s) g.Expect(err).To(HaveOccurred()) @@ -2358,10 +2349,10 @@ func TestReconcileMachineDeploymentsCleanup(t *testing.T) { s.Desired.MachineDeployments[md2WithTemplateChanges.Object.Name].Object.Namespace = "do-not-exist" r := Reconciler{ - Client: env.GetClient(), - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env.GetClient(), + APIReader: env.GetAPIReader(), + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileMachineDeployments(ctx, s) g.Expect(err).To(HaveOccurred()) @@ -2631,10 +2622,10 @@ func TestReconcileMachinePools(t *testing.T) { } r := Reconciler{ - Client: env.GetClient(), - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env.GetClient(), + APIReader: env.GetAPIReader(), + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileMachinePools(ctx, s) if tt.wantErr { @@ -2751,10 +2742,10 @@ func TestReconcileMachinePoolsCleanup(t *testing.T) { s.Desired.MachinePools[mp1.Object.Name].Object.Namespace = "do-not-exist" r := Reconciler{ - Client: env.GetClient(), - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env.GetClient(), + APIReader: env.GetAPIReader(), + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileMachinePools(ctx, s) g.Expect(err).To(HaveOccurred()) @@ -3183,9 +3174,9 @@ func TestReconcileReferencedObjectSequences(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } s := scope.New(&clusterv1.Cluster{}) @@ -3456,10 +3447,10 @@ func TestReconcileMachineDeploymentMachineHealthCheck(t *testing.T) { s.Desired = &scope.ClusterState{MachineDeployments: toMachineDeploymentTopologyStateMap(tt.desired)} r := Reconciler{ - Client: env.GetClient(), - APIReader: env.GetAPIReader(), - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env.GetClient(), + APIReader: env.GetAPIReader(), + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileMachineDeployments(ctx, s) @@ -3526,9 +3517,9 @@ func TestReconcileState(t *testing.T) { controlPlane.SetNamespace("do-not-exist") r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileState(ctx, s) g.Expect(err).To(HaveOccurred()) @@ -3573,9 +3564,9 @@ func TestReconcileState(t *testing.T) { prepareControlPlaneState(g, s.Desired.ControlPlane, namespace.GetName()) r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileState(ctx, s) g.Expect(err).ToNot(HaveOccurred()) @@ -3626,9 +3617,9 @@ func TestReconcileState(t *testing.T) { controlPlane.SetNamespace("do-not-exist") r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } err = r.reconcileState(ctx, s) g.Expect(err).To(HaveOccurred()) @@ -3798,9 +3789,9 @@ func TestReconciler_reconcileMachineHealthCheck(t *testing.T) { } r := Reconciler{ - Client: env, - patchHelperFactory: serverSideApplyPatchHelperFactory(env, ssa.NewCache(testController)), - recorder: env.GetEventRecorderFor("test"), + Client: env, + recorder: env.GetEventRecorderFor("test"), + ssaCache: ssa.NewCache("topology/cluster"), } if tt.current != nil { g.Expect(env.CreateAndWait(ctx, tt.current)).To(Succeed()) diff --git a/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper.go b/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper.go deleted file mode 100644 index b346088b8a7b..000000000000 --- a/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper.go +++ /dev/null @@ -1,253 +0,0 @@ -/* -Copyright 2021 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 structuredmerge - -import ( - "bytes" - "context" - "encoding/json" - - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - "sigs.k8s.io/cluster-api/internal/contract" - "sigs.k8s.io/cluster-api/internal/util/ssa" - "sigs.k8s.io/cluster-api/util" -) - -// TwoWaysPatchHelper helps with a patch that yields the modified document when applied to the original document. -type TwoWaysPatchHelper struct { - client client.Client - - // original holds the object to which the patch should apply to, to be used in the Patch method. - original client.Object - - // patch holds the merge patch in json format. - patch []byte - - // hasSpecChanges documents if the patch impacts the object spec - hasSpecChanges bool - changes []byte -} - -// NewTwoWaysPatchHelper will return a patch that yields the modified document when applied to the original document -// using the two-ways merge algorithm. -// NOTE: In the case of ClusterTopologyReconciler, original is the current object, modified is the desired object, and -// the patch returns all the changes required to align current to what is defined in desired; fields not managed -// by the topology controller are going to be preserved without changes. -// NOTE: TwoWaysPatch is considered a minimal viable replacement for server side apply during topology dry run, with -// the following limitations: -// - TwoWaysPatch doesn't consider OpenAPI schema extension like +ListMap this can lead to false positive when topology -// dry run is simulating a change to an existing slice -// (TwoWaysPatch always revert external changes, like server side apply when +ListMap=atomic). -// - TwoWaysPatch doesn't consider existing metadata.managedFields, and this can lead to false negative when topology dry run -// is simulating a change to an existing object where the topology controller is dropping an opinion for a field -// (TwoWaysPatch always preserve dropped fields, like server side apply when the field has more than one manager). -// - TwoWaysPatch doesn't generate metadata.managedFields as server side apply does. -// -// NOTE: NewTwoWaysPatchHelper consider changes only in metadata.labels, metadata.annotation and spec; it also respects -// the ignorePath option (same as the server side apply helper). -func NewTwoWaysPatchHelper(original, modified client.Object, c client.Client, opts ...HelperOption) (*TwoWaysPatchHelper, error) { - helperOptions := &HelperOptions{} - helperOptions = helperOptions.ApplyOptions(opts) - helperOptions.AllowedPaths = []contract.Path{ - {"metadata", "labels"}, - {"metadata", "annotations"}, - {"spec"}, // NOTE: The handling of managed path requires/assumes spec to be within allowed path. - } - // In case we are creating an object, we extend the set of allowed fields adding apiVersion, Kind - // metadata.name, metadata.namespace (who are required by the API server) and metadata.ownerReferences - // that gets set to avoid orphaned objects. - if util.IsNil(original) { - helperOptions.AllowedPaths = append(helperOptions.AllowedPaths, - contract.Path{"apiVersion"}, - contract.Path{"kind"}, - contract.Path{"metadata", "name"}, - contract.Path{"metadata", "namespace"}, - contract.Path{"metadata", "ownerReferences"}, - ) - } - - // Convert the input objects to json; if original is nil, use empty object so the - // following logic works without panicking. - originalJSON, err := json.Marshal(original) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal original object to json") - } - if util.IsNil(original) { - originalJSON = []byte("{}") - } - - modifiedJSON, err := json.Marshal(modified) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal modified object to json") - } - - // Apply patch options including: - // - exclude paths (fields to not consider, e.g. status); - // - ignore paths (well known fields owned by something else, e.g. spec.controlPlaneEndpoint in the - // InfrastructureCluster object); - // NOTE: All the above options trigger changes in the modified object so the resulting two ways patch - // includes or not the specific change. - modifiedJSON, err = applyOptions(&applyOptionsInput{ - original: originalJSON, - modified: modifiedJSON, - options: helperOptions, - }) - if err != nil { - return nil, errors.Wrap(err, "failed to apply options to modified") - } - - // Apply the modified object to the original one, merging the values of both; - // in case of conflicts, values from the modified object are preserved. - originalWithModifiedJSON, err := jsonpatch.MergePatch(originalJSON, modifiedJSON) - if err != nil { - return nil, errors.Wrap(err, "failed to apply modified json to original json") - } - - // Compute the merge patch that will align the original object to the target - // state defined above. - twoWayPatch, err := jsonpatch.CreateMergePatch(originalJSON, originalWithModifiedJSON) - if err != nil { - return nil, errors.Wrap(err, "failed to create merge patch") - } - - twoWayPatchMap := make(map[string]interface{}) - if err := json.Unmarshal(twoWayPatch, &twoWayPatchMap); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal two way merge patch") - } - - hasChanges := len(twoWayPatchMap) > 0 - // check if the changes impact the spec field. - hasSpecChanges := twoWayPatchMap["spec"] != nil - - var changes []byte - if hasChanges { - // Cleanup diff by dropping .metadata.managedFields. - ssa.FilterIntent(&ssa.FilterIntentInput{ - Path: contract.Path{}, - Value: twoWayPatchMap, - ShouldFilter: ssa.IsPathIgnored([]contract.Path{[]string{"metadata", "managedFields"}}), - }) - - changes, err = json.Marshal(twoWayPatchMap) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal diff") - } - } - - return &TwoWaysPatchHelper{ - client: c, - patch: twoWayPatch, - hasSpecChanges: hasSpecChanges, - changes: changes, - original: original, - }, nil -} - -type applyOptionsInput struct { - original []byte - modified []byte - options *HelperOptions -} - -// Apply patch options changing the modified object so the resulting two ways patch -// includes or not the specific change. -func applyOptions(in *applyOptionsInput) ([]byte, error) { - originalMap := make(map[string]interface{}) - if err := json.Unmarshal(in.original, &originalMap); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal original") - } - - modifiedMap := make(map[string]interface{}) - if err := json.Unmarshal(in.modified, &modifiedMap); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal modified") - } - - // drop changes for exclude paths (fields to not consider, e.g. status); - // Note: for everything not allowed it sets modified equal to original, so the generated patch doesn't include this change - if len(in.options.AllowedPaths) > 0 { - dropDiff(&dropDiffInput{ - path: contract.Path{}, - original: originalMap, - modified: modifiedMap, - shouldDropDiffFunc: ssa.IsPathNotAllowed(in.options.AllowedPaths), - }) - } - - // drop changes for ignore paths (well known fields owned by something else, e.g. - // spec.controlPlaneEndpoint in the InfrastructureCluster object); - // Note: for everything ignored it sets modified equal to original, so the generated patch doesn't include this change - if len(in.options.IgnorePaths) > 0 { - dropDiff(&dropDiffInput{ - path: contract.Path{}, - original: originalMap, - modified: modifiedMap, - shouldDropDiffFunc: ssa.IsPathIgnored(in.options.IgnorePaths), - }) - } - - modified, err := json.Marshal(&modifiedMap) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal modified") - } - - return modified, nil -} - -// HasSpecChanges return true if the patch has changes to the spec field. -func (h *TwoWaysPatchHelper) HasSpecChanges() bool { - return h.hasSpecChanges -} - -// Changes return the changes. -func (h *TwoWaysPatchHelper) Changes() []byte { - return h.changes -} - -// HasChanges return true if the patch has changes. -func (h *TwoWaysPatchHelper) HasChanges() bool { - return !bytes.Equal(h.patch, []byte("{}")) -} - -// Patch will attempt to apply the twoWaysPatch to the original object. -func (h *TwoWaysPatchHelper) Patch(ctx context.Context) error { - if !h.HasChanges() { - return nil - } - log := ctrl.LoggerFrom(ctx) - - if util.IsNil(h.original) { - modifiedMap := make(map[string]interface{}) - if err := json.Unmarshal(h.patch, &modifiedMap); err != nil { - return errors.Wrap(err, "failed to unmarshal two way merge patch") - } - - obj := &unstructured.Unstructured{ - Object: modifiedMap, - } - return h.client.Create(ctx, obj) - } - - // Note: deepcopy before patching in order to avoid modifications to the original object. - log.V(5).Info("Patching object", "patch", string(h.patch)) - return h.client.Patch(ctx, h.original.DeepCopyObject().(client.Object), client.RawPatch(types.MergePatchType, h.patch)) -} diff --git a/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper_test.go b/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper_test.go deleted file mode 100644 index 307ab29e84a3..000000000000 --- a/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper_test.go +++ /dev/null @@ -1,452 +0,0 @@ -/* -Copyright 2021 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 structuredmerge - -import ( - "fmt" - "testing" - - . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "sigs.k8s.io/cluster-api/internal/contract" - "sigs.k8s.io/cluster-api/util/test/builder" -) - -func TestNewHelper(t *testing.T) { - tests := []struct { - name string - original *unstructured.Unstructured // current - modified *unstructured.Unstructured // desired - options []HelperOption - wantHasChanges bool - wantHasSpecChanges bool - wantPatch []byte - }{ - // Create - - { - name: "Create if original does not exists", - original: nil, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "apiVersion": builder.BootstrapGroupVersion.String(), - "kind": builder.GenericBootstrapConfigKind, - "metadata": map[string]interface{}{ - "namespace": metav1.NamespaceDefault, - "name": "foo", - }, - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - options: []HelperOption{}, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte(fmt.Sprintf("{\"apiVersion\":%q,\"kind\":%q,\"metadata\":{\"name\":\"foo\",\"namespace\":%q},\"spec\":{\"foo\":\"foo\"}}", builder.BootstrapGroupVersion.String(), builder.GenericBootstrapConfigKind, metav1.NamespaceDefault)), - }, - - // Ignore fields - - { - name: "Ignore fields are removed from the patch", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{}, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "controlPlaneEndpoint": map[string]interface{}{ - "host": "", - "port": int64(0), - }, - }, - }, - }, - options: []HelperOption{IgnorePaths{contract.Path{"spec", "controlPlaneEndpoint"}}}, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - - // Allowed Path fields - - { - name: "Not allowed fields are removed from the patch", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{}, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "status": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - - // Field both in original and in modified --> align to modified if different - - { - name: "Field (spec.foo) both in original and in modified, no-op when equal", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - { - name: "Field (metadata.label) both in original and in modified, align to modified when different", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "foo": "foo-modified", - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: false, - wantPatch: []byte("{\"metadata\":{\"labels\":{\"foo\":\"foo-modified\"}}}"), - }, - { - name: "Field (spec.template.spec.foo) both in original and in modified, no-op when equal", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - - { - name: "Field (spec.foo) both in original and in modified, align to modified when different", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo-changed", - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte("{\"spec\":{\"foo\":\"foo-changed\"}}"), - }, - { - name: "Field (metadata.label) both in original and in modified, align to modified when different", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "foo": "foo-changed", - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: false, - wantPatch: []byte("{\"metadata\":{\"labels\":{\"foo\":\"foo-changed\"}}}"), - }, - { - name: "Field (spec.template.spec.foo) both in original and in modified, align to modified when different", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo-changed", - }, - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte("{\"spec\":{\"template\":{\"spec\":{\"foo\":\"foo-changed\"}}}}"), - }, - - { - name: "Value of type Array or Slice both in original and in modified,, align to modified when different", // Note: fake treats all the slice as atomic (false positive) - original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "slice": []interface{}{ - "D", - "C", - "B", - }, - }, - }, - }, - modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "slice": []interface{}{ - "A", - "B", - "C", - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte("{\"spec\":{\"slice\":[\"A\",\"B\",\"C\"]}}"), - }, - - // Field only in modified (not existing in original) --> align to modified - - { - name: "Field (spec.foo) in modified only, align to modified", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{}, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo-changed", - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte("{\"spec\":{\"foo\":\"foo-changed\"}}"), - }, - { - name: "Field (metadata.label) in modified only, align to modified", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{}, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "foo": "foo-changed", - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: false, - wantPatch: []byte("{\"metadata\":{\"labels\":{\"foo\":\"foo-changed\"}}}"), - }, - { - name: "Field (spec.template.spec.foo) in modified only, align to modified when different", - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{}, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo-changed", - }, - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte("{\"spec\":{\"template\":{\"spec\":{\"foo\":\"foo-changed\"}}}}"), - }, - - { - name: "Value of type Array or Slice in modified only, align to modified when different", - original: &unstructured.Unstructured{ - Object: map[string]interface{}{}, - }, - modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "slice": []interface{}{ - "A", - "B", - "C", - }, - }, - }, - }, - wantHasChanges: true, - wantHasSpecChanges: true, - wantPatch: []byte("{\"spec\":{\"slice\":[\"A\",\"B\",\"C\"]}}"), - }, - - // Field only in original (not existing in modified) --> preserve original - - { - name: "Field (spec.foo) in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers, so it assumes (false negative) - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{}, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - { - name: "Field (metadata.label) in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers (false negative) - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{}, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - { - name: "Field (spec.template.spec.foo) in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers (false negative) - original: &unstructured.Unstructured{ // current - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "foo": "foo", - }, - }, - }, - }, - }, - modified: &unstructured.Unstructured{ // desired - Object: map[string]interface{}{}, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - - { - name: "Value of type Array or Slice in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers (false negative) - original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "slice": []interface{}{ - "D", - "C", - "B", - }, - }, - }, - }, - modified: &unstructured.Unstructured{ - Object: map[string]interface{}{}, - }, - wantHasChanges: false, - wantHasSpecChanges: false, - wantPatch: []byte("{}"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - patch, err := NewTwoWaysPatchHelper(tt.original, tt.modified, env.GetClient(), tt.options...) - g.Expect(err).ToNot(HaveOccurred()) - - g.Expect(patch.patch).To(Equal(tt.wantPatch)) - g.Expect(patch.HasChanges()).To(Equal(tt.wantHasChanges)) - g.Expect(patch.HasSpecChanges()).To(Equal(tt.wantHasSpecChanges)) - }) - } -} diff --git a/internal/controllers/topology/cluster/util_test.go b/internal/controllers/topology/cluster/util_test.go index 5be4323d6aef..0673d095707e 100644 --- a/internal/controllers/topology/cluster/util_test.go +++ b/internal/controllers/topology/cluster/util_test.go @@ -99,8 +99,7 @@ func TestGetReference(t *testing.T) { Build() r := &Reconciler{ - Client: fakeClient, - patchHelperFactory: dryRunPatchHelperFactory(fakeClient), + Client: fakeClient, } got, err := r.getReference(ctx, tt.ref) if tt.wantErr {