sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/internal/dryrun/client.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package dryrun
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	"github.com/pkg/errors"
    24  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    31  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    32  
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    35  )
    36  
    37  var (
    38  	localScheme = scheme.Scheme
    39  )
    40  
    41  // changeTrackerID represents a unique identifier of an object.
    42  type changeTrackerID struct {
    43  	gvk schema.GroupVersionKind
    44  	key client.ObjectKey
    45  }
    46  
    47  type operationType string
    48  
    49  const (
    50  	// Represents that a new object is created.
    51  	opCreate operationType = "create"
    52  
    53  	// Represents that the object is modified.
    54  	// This could be a result of performing Patch or Update or delete and re-create operations on the object.
    55  	opModify operationType = "modify"
    56  
    57  	// Represents that the object is deleted.
    58  	opDelete operationType = "delete"
    59  )
    60  
    61  // operation represents the final effective operation and the original object (initial state) associated
    62  // with the operation.
    63  type operation struct {
    64  	originalValue client.Object
    65  	operation     operationType
    66  }
    67  
    68  // changeTracker is used to track the operations performed on the objects.
    69  // changeTracker flattens all operations performed on the same object and
    70  // only tracks the final effective operation on the object when compared to
    71  // the initial state. Example: If an object is created and later modified
    72  // it is only tracked as created (effective final operation).
    73  //
    74  // While changeTracker tracks the operations on objects using unique object identifiers
    75  // ChangeSummary reports all the operations and the final state of objects. ChangeSummary
    76  // is calculated using changeTracker and fake client.
    77  type changeTracker struct {
    78  	changes map[changeTrackerID]*operation
    79  }
    80  
    81  // Client implements a dry run Client, that is a fake.Client that logs write operations.
    82  type Client struct {
    83  	fakeClient client.Client
    84  	apiReader  client.Reader
    85  
    86  	changeTracker *changeTracker
    87  }
    88  
    89  // PatchSummary defines the patch observed on an object.
    90  type PatchSummary struct {
    91  	// Initial state of the object.
    92  	Before *unstructured.Unstructured
    93  	// Final state of the object.
    94  	After *unstructured.Unstructured
    95  }
    96  
    97  // ChangeSummary defines all the changes detected by the Dryrun execution.
    98  // Nb. Only a single operation is reported for each object, flattening operations
    99  // to show difference between the initial and final states.
   100  type ChangeSummary struct {
   101  	// Created is the list of objects that are created during the dry run execution.
   102  	Created []*unstructured.Unstructured
   103  
   104  	// Modified is the list of summary of objects that are modified (Updated, Patched and/or deleted and re-created) during the dry run execution.
   105  	Modified []*PatchSummary
   106  
   107  	// Deleted is the list of objects that are deleted during the dry run execution.
   108  	Deleted []*unstructured.Unstructured
   109  }
   110  
   111  // NewClient returns a new dry run Client.
   112  // A dry run client mocks interactions with an api server using a fake internal object tracker.
   113  // The objects passed will be used to initialize the fake internal object tracker when creating a new dry run client.
   114  // If an apiReader client is passed the dry run client will use it as a fall back client for read operations (Get, List)
   115  // when the objects are not found in the internal object tracker. Typically the apiReader passed would be a reader client
   116  // to a real Kubernetes Cluster.
   117  func NewClient(apiReader client.Reader, objs []client.Object) *Client {
   118  	fakeClient := fake.NewClientBuilder().WithObjects(objs...).WithStatusSubresource(&clusterv1.ClusterClass{}, &clusterv1.Cluster{}).WithScheme(localScheme).Build()
   119  	return &Client{
   120  		fakeClient: fakeClient,
   121  		apiReader:  apiReader,
   122  		changeTracker: &changeTracker{
   123  			changes: map[changeTrackerID]*operation{},
   124  		},
   125  	}
   126  }
   127  
   128  // Get retrieves an object for the given object key from the internal object tracker.
   129  // If the object does not exist in the internal object tracker it tries to fetch the object
   130  // from the Kubernetes Cluster using the apiReader client (if apiReader is not nil).
   131  func (c *Client) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
   132  	if err := c.fakeClient.Get(ctx, key, obj, opts...); err != nil {
   133  		// If the object is not found by the fake client, get the object
   134  		// using the apiReader.
   135  		if apierrors.IsNotFound(err) && c.apiReader != nil {
   136  			return c.apiReader.Get(ctx, key, obj, opts...)
   137  		}
   138  		return err
   139  	}
   140  	return nil
   141  }
   142  
   143  // List retrieves list of objects for a given namespace and list options.
   144  // List function returns the union of the lists from the internal object tracker and the Kubernetes Cluster.
   145  // Nb. For objects that exist both in the internal object tracker and the Kubernetes Cluster, internal object tracker
   146  // takes precedence.
   147  func (c *Client) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
   148  	var gvk schema.GroupVersionKind
   149  	if uList, ok := list.(*unstructured.UnstructuredList); ok {
   150  		gvk = uList.GroupVersionKind()
   151  	} else {
   152  		var err error
   153  		gvk, err = apiutil.GVKForObject(list, c.fakeClient.Scheme())
   154  		if err != nil {
   155  			return errors.Wrap(err, "failed to get GVK of target object")
   156  		}
   157  	}
   158  
   159  	// Fetch lists from both fake client and the apiReader and merge the two lists.
   160  	unstructuredFakeList := &unstructured.UnstructuredList{}
   161  	unstructuredFakeList.SetGroupVersionKind(gvk)
   162  	if err := c.fakeClient.List(ctx, unstructuredFakeList, opts...); err != nil {
   163  		return err
   164  	}
   165  	if c.apiReader != nil {
   166  		unstructuredReaderList := &unstructured.UnstructuredList{}
   167  		unstructuredReaderList.SetGroupVersionKind(gvk)
   168  		if err := c.apiReader.List(ctx, unstructuredReaderList, opts...); err != nil {
   169  			return err
   170  		}
   171  		mergeLists(unstructuredFakeList, unstructuredReaderList)
   172  	}
   173  
   174  	if err := c.Scheme().Convert(unstructuredFakeList, list, nil); err != nil {
   175  		return errors.Wrapf(err, "failed to convert unstructured list to %T", list)
   176  	}
   177  	return nil
   178  }
   179  
   180  // Create saves the object in the internal object tracker.
   181  func (c *Client) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
   182  	err := c.fakeClient.Create(ctx, obj, opts...)
   183  	if err == nil {
   184  		id := trackerIDFor(obj)
   185  		// If the object was previously deleted, it is now being re-created. Effectively it is a modify operation
   186  		// on the object.
   187  		if op, ok := c.changeTracker.changes[id]; ok {
   188  			if op.operation == opDelete {
   189  				op.operation = opModify
   190  			}
   191  		} else {
   192  			// This is the first operation on this object. Track the create operation.
   193  			c.changeTracker.changes[id] = &operation{
   194  				operation: opCreate,
   195  			}
   196  		}
   197  	}
   198  	return err
   199  }
   200  
   201  // Delete deletes the given obj from internal object tracker.
   202  // Delete will not affect objects in the Kubernetes Cluster.
   203  func (c *Client) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
   204  	err := c.fakeClient.Delete(ctx, obj, opts...)
   205  	if err != nil {
   206  		if !apierrors.IsNotFound(err) {
   207  			return err
   208  		}
   209  		if c.apiReader == nil {
   210  			return err
   211  		}
   212  		// It is possible that we are trying to delete an object that exists in the Kubernetes Cluster but
   213  		// not in the internal object tracker.
   214  		// In such cases, check if the underlying object exists and if the object does
   215  		// not exist return the original error.
   216  		tmpObj := obj.DeepCopyObject().(client.Object)
   217  		if getErr := c.apiReader.Get(ctx, client.ObjectKeyFromObject(obj), tmpObj); getErr != nil {
   218  			if apierrors.IsNotFound(getErr) {
   219  				// Delete was called on an object that does no exists in the internal object tracker and in the
   220  				// Kubernetes Cluster. Return error.
   221  				// Note: return the original delete error. Not the get error.
   222  				return err
   223  			}
   224  			return errors.Wrap(err, "failed to check if object exists in underlying cluster")
   225  		}
   226  	}
   227  	// If the object is already tracked under a different operation we need to adjust the effective
   228  	// operation using the following rules:
   229  	// - If the object is tracked as created, drop the tracking. Effective operation is object never existed.
   230  	// - If the object is tracked in modified, change to deleted. Effective operation is object is deleted.
   231  	id := trackerIDFor(obj)
   232  	if op, ok := c.changeTracker.changes[id]; ok {
   233  		if op.operation == opCreate {
   234  			delete(c.changeTracker.changes, id)
   235  		}
   236  		if op.operation == opModify {
   237  			op.operation = opDelete
   238  		}
   239  	} else {
   240  		// The object is observed for the first time.
   241  		// Track the delete operation on the object.
   242  		c.changeTracker.changes[id] = &operation{
   243  			originalValue: obj,
   244  			operation:     opDelete,
   245  		}
   246  	}
   247  	return nil
   248  }
   249  
   250  // Update updates the given obj in the internal object tracker.
   251  // NOTE: Topology reconciler does not use update, so we are skipping implementation for now.
   252  func (c *Client) Update(_ context.Context, _ client.Object, _ ...client.UpdateOption) error {
   253  	panic("Update method is not supported by the dryrun client")
   254  }
   255  
   256  // Patch patches the given obj in the internal object tracker.
   257  // The patch operation will be tracked if the object does not exist in the internal object tracker but exists in the Kubernetes Cluster.
   258  func (c *Client) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
   259  	originalObj := obj.DeepCopyObject().(client.Object)
   260  	// The fake client Patch operation internally makes a Get call. Therefore,
   261  	// create the object if it does not exist in the fake object tracker using the fake client.
   262  	// Note: Because of this operation we will nullify any real errors caused by calling Patch on an object that does no exist.
   263  	// Such cases can only occur because of bugs in reconciler. The dry run operation is not meant to capture bugs in the reconciler
   264  	// hence we choose to ignore such edge cases.
   265  	if err := c.ensureObjInFakeClient(ctx, obj); err != nil {
   266  		return errors.Wrap(err, "failed to ensure object is available in fake object tracker")
   267  	}
   268  	err := c.fakeClient.Patch(ctx, obj, patch, opts...)
   269  	if err == nil {
   270  		id := trackerIDFor(obj)
   271  		// If the object is not already tracked, track the modify operation.
   272  		// If the object is already tracked we don't need to perform any further action because of the following:
   273  		// - Tracked as created - created takes precedence over modified.
   274  		// - Tracked as modified - the object is already tracked with the correct operation.
   275  		// - Tracked as deleted - case not possible. Object cannot be patched after it is deleted.
   276  		if _, ok := c.changeTracker.changes[id]; !ok {
   277  			c.changeTracker.changes[id] = &operation{
   278  				originalValue: originalObj,
   279  				operation:     opModify,
   280  			}
   281  		}
   282  	}
   283  	return err
   284  }
   285  
   286  // DeleteAllOf deletes all objects of the given type matching the given options.
   287  // NOTE: Topology reconciler does not use DeleteAllOf, so we are skipping implementation for now.
   288  func (c *Client) DeleteAllOf(_ context.Context, _ client.Object, _ ...client.DeleteAllOfOption) error {
   289  	panic("DeleteAllOf method is not supported by the dryrun client")
   290  }
   291  
   292  // Status returns a client which can update the status subresource for Kubernetes objects.
   293  func (c *Client) Status() client.StatusWriter {
   294  	return c.fakeClient.Status()
   295  }
   296  
   297  // Scheme returns the scheme this client is using.
   298  func (c *Client) Scheme() *runtime.Scheme {
   299  	return c.fakeClient.Scheme()
   300  }
   301  
   302  // RESTMapper returns the rest this client is using.
   303  func (c *Client) RESTMapper() meta.RESTMapper {
   304  	return c.fakeClient.RESTMapper()
   305  }
   306  
   307  // SubResource returns the sub resource this client is using.
   308  func (c *Client) SubResource(subResource string) client.SubResourceClient {
   309  	return c.fakeClient.SubResource(subResource)
   310  }
   311  
   312  // GroupVersionKindFor returns the GroupVersionKind for the given object.
   313  func (c *Client) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
   314  	return c.fakeClient.GroupVersionKindFor(obj)
   315  }
   316  
   317  // IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced.
   318  func (c *Client) IsObjectNamespaced(obj runtime.Object) (bool, error) {
   319  	return c.fakeClient.IsObjectNamespaced(obj)
   320  }
   321  
   322  // Changes generates a summary of all the changes observed from the creation of the dry run client
   323  // to when this function is called.
   324  func (c *Client) Changes(ctx context.Context) (*ChangeSummary, error) {
   325  	changes := &ChangeSummary{
   326  		Created:  []*unstructured.Unstructured{},
   327  		Modified: []*PatchSummary{},
   328  		Deleted:  []*unstructured.Unstructured{},
   329  	}
   330  
   331  	for id, op := range c.changeTracker.changes {
   332  		switch op.operation {
   333  		case opCreate:
   334  			obj := &unstructured.Unstructured{}
   335  			obj.SetGroupVersionKind(id.gvk)
   336  			obj.SetNamespace(id.key.Namespace)
   337  			obj.SetName(id.key.Name)
   338  			if err := c.fakeClient.Get(ctx, id.key, obj); err != nil {
   339  				return nil, errors.Wrapf(err, "failed to read created object %s", id.key.String())
   340  			}
   341  			changes.Created = append(changes.Created, obj)
   342  		case opModify:
   343  			// Get the final object.
   344  			after := &unstructured.Unstructured{}
   345  			after.SetGroupVersionKind(id.gvk)
   346  			after.SetNamespace(id.key.Namespace)
   347  			after.SetName(id.key.Name)
   348  			if err := c.fakeClient.Get(ctx, id.key, after); err != nil {
   349  				return nil, errors.Wrapf(err, "failed to read modified object %s", id.key.String())
   350  			}
   351  			// Get the initial object.
   352  			before := &unstructured.Unstructured{}
   353  			if err := c.Scheme().Convert(op.originalValue, before, nil); err != nil {
   354  				return nil, errors.Wrapf(err, "failed to convert %s to unstructured", client.ObjectKeyFromObject(op.originalValue).String())
   355  			}
   356  			changes.Modified = append(changes.Modified, &PatchSummary{
   357  				Before: before,
   358  				After:  after,
   359  			})
   360  		case opDelete:
   361  			obj := &unstructured.Unstructured{}
   362  			if err := c.Scheme().Convert(op.originalValue, obj, nil); err != nil {
   363  				return nil, errors.Wrapf(err, "failed to convert %s to unstructured", client.ObjectKeyFromObject(op.originalValue).String())
   364  			}
   365  			changes.Deleted = append(changes.Deleted, obj)
   366  		default:
   367  			return nil, fmt.Errorf("untracked operation detected")
   368  		}
   369  	}
   370  
   371  	return changes, nil
   372  }
   373  
   374  // ensureObjInFakeClient makes sure that the object is available in the fake client.
   375  // If the object is not already available it will add it to the fake client by running a "Create"
   376  // operation.
   377  func (c *Client) ensureObjInFakeClient(ctx context.Context, obj client.Object) error {
   378  	o := obj.DeepCopyObject().(client.Object)
   379  	// During create object should not have resourceVersion.
   380  	o.SetResourceVersion("")
   381  	if err := c.fakeClient.Create(ctx, o); err != nil {
   382  		if apierrors.IsAlreadyExists(err) {
   383  			// If the object already exists it is okay for create to fail.
   384  			return nil
   385  		}
   386  		return errors.Wrap(err, "failed to add object to fake object tracker")
   387  	}
   388  	return nil
   389  }
   390  
   391  // mergeLists merges the 2 lists a and b by adding every item in b
   392  // that is not in a to list a.
   393  // List a will be merged list.
   394  func mergeLists(a, b *unstructured.UnstructuredList) {
   395  	keyGen := func(u *unstructured.Unstructured) string {
   396  		return fmt.Sprintf("%s-%s", u.GroupVersionKind().String(), client.ObjectKeyFromObject(u).String())
   397  	}
   398  	keys := map[string]bool{}
   399  	// Generate all unique keys for the items in list a.
   400  	for i := range a.Items {
   401  		keys[keyGen(&a.Items[i])] = true
   402  	}
   403  	// For every item in b that is not in a add it to a.
   404  	for i := range b.Items {
   405  		if _, ok := keys[keyGen(&b.Items[i])]; !ok {
   406  			a.Items = append(a.Items, b.Items[i])
   407  		}
   408  	}
   409  }
   410  
   411  func trackerIDFor(o client.Object) changeTrackerID {
   412  	return changeTrackerID{
   413  		gvk: o.GetObjectKind().GroupVersionKind(),
   414  		key: client.ObjectKeyFromObject(o),
   415  	}
   416  }