sigs.k8s.io/cluster-api@v1.7.1/internal/hooks/tracking.go (about)

     1  /*
     2  Copyright 2021 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 hooks has helper functions for Runtime Hooks.
    18  package hooks
    19  
    20  import (
    21  	"context"
    22  	"strings"
    23  
    24  	"github.com/pkg/errors"
    25  	"k8s.io/apimachinery/pkg/util/sets"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  
    28  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    29  	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
    30  	tlog "sigs.k8s.io/cluster-api/internal/log"
    31  	"sigs.k8s.io/cluster-api/util/patch"
    32  )
    33  
    34  // MarkAsPending adds to the object's PendingHooksAnnotation the intent to execute a hook after an operation completes.
    35  // Usually this function is called when an operation is starting in order to track the intent to call an After<operation> hook later in the process.
    36  func MarkAsPending(ctx context.Context, c client.Client, obj client.Object, hooks ...runtimecatalog.Hook) error {
    37  	hookNames := []string{}
    38  	for _, hook := range hooks {
    39  		hookNames = append(hookNames, runtimecatalog.HookName(hook))
    40  	}
    41  
    42  	patchHelper, err := patch.NewHelper(obj, c)
    43  	if err != nil {
    44  		return errors.Wrapf(err, "failed to mark %q hook(s) as pending", strings.Join(hookNames, ","))
    45  	}
    46  
    47  	// Read the annotation of the objects and add the hook to the comma separated list
    48  	annotations := obj.GetAnnotations()
    49  	if annotations == nil {
    50  		annotations = map[string]string{}
    51  	}
    52  	annotations[runtimev1.PendingHooksAnnotation] = addToCommaSeparatedList(annotations[runtimev1.PendingHooksAnnotation], hookNames...)
    53  	obj.SetAnnotations(annotations)
    54  
    55  	if err := patchHelper.Patch(ctx, obj); err != nil {
    56  		return errors.Wrapf(err, "failed to mark %q hook(s) as pending", strings.Join(hookNames, ","))
    57  	}
    58  
    59  	return nil
    60  }
    61  
    62  // IsPending returns true if there is an intent to call a hook being tracked in the object's PendingHooksAnnotation.
    63  func IsPending(hook runtimecatalog.Hook, obj client.Object) bool {
    64  	hookName := runtimecatalog.HookName(hook)
    65  	annotations := obj.GetAnnotations()
    66  	if annotations == nil {
    67  		return false
    68  	}
    69  	return isInCommaSeparatedList(annotations[runtimev1.PendingHooksAnnotation], hookName)
    70  }
    71  
    72  // MarkAsDone removes the intent to call a Hook from the object's PendingHooksAnnotation.
    73  // Usually this func is called after all the registered extensions for the Hook returned an answer without requests
    74  // to hold on to the object's lifecycle (retryAfterSeconds).
    75  func MarkAsDone(ctx context.Context, c client.Client, obj client.Object, hooks ...runtimecatalog.Hook) error {
    76  	hookNames := []string{}
    77  	for _, hook := range hooks {
    78  		hookNames = append(hookNames, runtimecatalog.HookName(hook))
    79  	}
    80  
    81  	patchHelper, err := patch.NewHelper(obj, c)
    82  	if err != nil {
    83  		return errors.Wrapf(err, "failed to mark %q hook(s) as done", strings.Join(hookNames, ","))
    84  	}
    85  
    86  	// Read the annotation of the objects and add the hook to the comma separated list
    87  	annotations := obj.GetAnnotations()
    88  	if annotations == nil {
    89  		annotations = map[string]string{}
    90  	}
    91  	annotations[runtimev1.PendingHooksAnnotation] = removeFromCommaSeparatedList(annotations[runtimev1.PendingHooksAnnotation], hookNames...)
    92  	if annotations[runtimev1.PendingHooksAnnotation] == "" {
    93  		delete(annotations, runtimev1.PendingHooksAnnotation)
    94  	}
    95  	obj.SetAnnotations(annotations)
    96  
    97  	if err := patchHelper.Patch(ctx, obj); err != nil {
    98  		return errors.Wrapf(err, "failed to mark %q hook(s) as done", strings.Join(hookNames, ","))
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  // IsOkToDelete returns true if object has the OkToDeleteAnnotation in the annotations of the object, false otherwise.
   105  func IsOkToDelete(obj client.Object) bool {
   106  	annotations := obj.GetAnnotations()
   107  	if annotations == nil {
   108  		return false
   109  	}
   110  	if _, ok := annotations[runtimev1.OkToDeleteAnnotation]; ok {
   111  		return true
   112  	}
   113  	return false
   114  }
   115  
   116  // MarkAsOkToDelete adds the OkToDeleteAnnotation annotation to the object and patches it.
   117  func MarkAsOkToDelete(ctx context.Context, c client.Client, obj client.Object) error {
   118  	patchHelper, err := patch.NewHelper(obj, c)
   119  	if err != nil {
   120  		return errors.Wrapf(err, "failed to mark %s as ok to delete", tlog.KObj{Obj: obj})
   121  	}
   122  
   123  	annotations := obj.GetAnnotations()
   124  	if annotations == nil {
   125  		annotations = map[string]string{}
   126  	}
   127  	annotations[runtimev1.OkToDeleteAnnotation] = ""
   128  	obj.SetAnnotations(annotations)
   129  
   130  	if err := patchHelper.Patch(ctx, obj); err != nil {
   131  		return errors.Wrapf(err, "failed to mark %s as ok to delete", tlog.KObj{Obj: obj})
   132  	}
   133  
   134  	return nil
   135  }
   136  
   137  func addToCommaSeparatedList(list string, items ...string) string {
   138  	set := sets.Set[string]{}.Insert(strings.Split(list, ",")...)
   139  
   140  	// Remove empty strings (that might have been introduced by splitting an empty list)
   141  	// from the hook list
   142  	set.Delete("")
   143  
   144  	set.Insert(items...)
   145  
   146  	return strings.Join(sets.List(set), ",")
   147  }
   148  
   149  func isInCommaSeparatedList(list, item string) bool {
   150  	set := sets.Set[string]{}.Insert(strings.Split(list, ",")...)
   151  	return set.Has(item)
   152  }
   153  
   154  func removeFromCommaSeparatedList(list string, items ...string) string {
   155  	set := sets.Set[string]{}.Insert(strings.Split(list, ",")...)
   156  
   157  	// Remove empty strings (that might have been introduced by splitting an empty list)
   158  	// from the hook list
   159  	set.Delete("")
   160  
   161  	set.Delete(items...)
   162  	return strings.Join(sets.List(set), ",")
   163  }