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 }