...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s/meta.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package k8s
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  
    23  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/lease/leasable"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    26  
    27  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    28  	"github.com/nasa9084/go-openapi"
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  )
    36  
    37  func GetNamespacedName(obj metav1.Object) types.NamespacedName {
    38  	return types.NamespacedName{
    39  		Namespace: obj.GetNamespace(),
    40  		Name:      obj.GetName(),
    41  	}
    42  }
    43  
    44  func IsManagedByKCC(gvk schema.GroupVersionKind) bool {
    45  	return strings.HasSuffix(gvk.Group, CNRMGroup)
    46  }
    47  
    48  func IsDeleted(objectMeta *metav1.ObjectMeta) bool {
    49  	return !objectMeta.DeletionTimestamp.IsZero()
    50  }
    51  
    52  func HasAbandonAnnotation(obj metav1.Object) bool {
    53  	val, ok := GetAnnotation(DeletionPolicyAnnotation, obj)
    54  	return ok && val == DeletionPolicyAbandon
    55  }
    56  
    57  func GVKListContains(gvkList []schema.GroupVersionKind, gvk schema.GroupVersionKind) bool {
    58  	for _, v := range gvkList {
    59  		if v == gvk {
    60  			return true
    61  		}
    62  	}
    63  	return false
    64  }
    65  
    66  func GVKSetToList(gvkSet map[schema.GroupVersionKind]bool) []schema.GroupVersionKind {
    67  	gvkList := make([]schema.GroupVersionKind, 0, len(gvkSet))
    68  	for gvk := range gvkSet {
    69  		gvkList = append(gvkList, gvk)
    70  	}
    71  	return gvkList
    72  }
    73  
    74  func SortGVKsByKind(gvks []schema.GroupVersionKind) []schema.GroupVersionKind {
    75  	gvksCopy := append(make([]schema.GroupVersionKind, 0), gvks...)
    76  	sort.Slice(gvksCopy, func(i, j int) bool {
    77  		return gvksCopy[i].Kind < gvksCopy[j].Kind
    78  	})
    79  	return gvksCopy
    80  }
    81  
    82  // ToGVR returns the equivalent GVR for a given GVK. Note that while GVKs and
    83  // GVRs do not necessarily have a 1:1 mapping, GVKs and GVRs of CRDs do.
    84  // (see https://book.kubebuilder.io/cronjob-tutorial/gvks.html#kinds-and-resources)
    85  func ToGVR(gvk schema.GroupVersionKind) schema.GroupVersionResource {
    86  	return schema.GroupVersionResource{
    87  		Group:    gvk.Group,
    88  		Version:  gvk.Version,
    89  		Resource: text.Pluralize(strings.ToLower(gvk.Kind)),
    90  	}
    91  }
    92  
    93  func GetProjectIDForNamespace(c client.Client, ctx context.Context, namespaceName string) (string, error) {
    94  	var ns corev1.Namespace
    95  	if err := c.Get(ctx, types.NamespacedName{Name: namespaceName}, &ns); err != nil {
    96  		return "", fmt.Errorf("error getting namespace '%v': %v", namespaceName, err)
    97  	}
    98  	if val, ok := GetAnnotation(ProjectIDAnnotation, &ns); ok {
    99  		return val, nil
   100  	}
   101  	return namespaceName, nil
   102  }
   103  
   104  func GetAnnotation(annotation string, obj metav1.Object) (string, bool) {
   105  	annotations := obj.GetAnnotations()
   106  	if annotations == nil {
   107  		return "", false
   108  	}
   109  	val, ok := annotations[annotation]
   110  	return val, ok
   111  }
   112  
   113  func SetAnnotation(annotation, val string, obj metav1.Object) {
   114  	annotations := obj.GetAnnotations()
   115  	if annotations == nil {
   116  		annotations = make(map[string]string)
   117  	}
   118  	annotations[annotation] = val
   119  	obj.SetAnnotations(annotations)
   120  }
   121  
   122  func RemoveAnnotation(annotation string, obj metav1.Object) {
   123  	annotations := obj.GetAnnotations()
   124  	if annotations == nil {
   125  		return
   126  	}
   127  	delete(annotations, annotation)
   128  	obj.SetAnnotations(annotations)
   129  }
   130  
   131  func GetManagementConflictPreventionAnnotationValue(obj metav1.Object) (ManagementConflictPreventionPolicy, error) {
   132  	if val, ok := GetAnnotation(ManagementConflictPreventionPolicyFullyQualifiedAnnotation, obj); ok {
   133  		return valueToManagementConflictPreventionPolicy(val)
   134  	}
   135  	return ManagementConflictPreventionPolicyNone,
   136  		fmt.Errorf("attempted to get value for annotation %v, but annotation was not found", ManagementConflictPreventionPolicyFullyQualifiedAnnotation)
   137  }
   138  
   139  func EnsureManagementConflictPreventionAnnotationForTFBasedResource(c client.Client, ctx context.Context, obj metav1.Object, rc *corekccv1alpha1.ResourceConfig, tfResourceMap map[string]*tfschema.Resource) error {
   140  	ns := corev1.Namespace{}
   141  	if err := c.Get(ctx, types.NamespacedName{Name: obj.GetNamespace()}, &ns); err != nil {
   142  		return fmt.Errorf("error getting namespace %v: %v", obj.GetNamespace(), err)
   143  	}
   144  	return ValidateOrDefaultManagementConflictPreventionAnnotationForTFBasedResource(obj, &ns, rc, tfResourceMap)
   145  }
   146  
   147  func ValidateOrDefaultManagementConflictPreventionAnnotationForTFBasedResource(obj metav1.Object, ns *corev1.Namespace, rc *corekccv1alpha1.ResourceConfig, tfResourceMap map[string]*tfschema.Resource) error {
   148  	supportsLeasing, err := leasable.ResourceConfigSupportsLeasing(rc, tfResourceMap)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	return validateOrDefaultManagementConflictPreventionAnnotation(obj, ns, supportsLeasing)
   153  }
   154  
   155  func ValidateOrDefaultManagementConflictPreventionAnnotationForDCLBasedResource(obj metav1.Object, ns *corev1.Namespace, schema *openapi.Schema) error {
   156  	supportsLeasing, err := leasable.DCLSchemaSupportsLeasing(schema)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	return validateOrDefaultManagementConflictPreventionAnnotation(obj, ns, supportsLeasing)
   161  }
   162  
   163  func validateOrDefaultManagementConflictPreventionAnnotation(obj metav1.Object, ns *corev1.Namespace, supportsLeasing bool) error {
   164  	value, ok := GetAnnotation(ManagementConflictPreventionPolicyFullyQualifiedAnnotation, obj)
   165  	if ok {
   166  		// the value is supplied by the customer so ensure it is valid
   167  		policy, err := valueToManagementConflictPreventionPolicy(value)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		return validateManagementConflictPolicyForResource(policy, supportsLeasing)
   172  	}
   173  	policy, err := getDefaultManagementConflictPreventAnnotationForNamespace(ns, supportsLeasing)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	SetAnnotation(ManagementConflictPreventionPolicyFullyQualifiedAnnotation, string(policy), obj)
   178  	return nil
   179  }
   180  
   181  func getDefaultManagementConflictPreventAnnotationForNamespace(ns *corev1.Namespace, supportLeasing bool) (ManagementConflictPreventionPolicy,
   182  	error) {
   183  	value, ok := GetAnnotation(ManagementConflictPreventionPolicyFullyQualifiedAnnotation, ns)
   184  	if ok {
   185  		policy, err := valueToManagementConflictPreventionPolicy(value)
   186  		if err != nil {
   187  			return ManagementConflictPreventionPolicyNone, fmt.Errorf("unable to use default management conflict policy for namespace: %v", err)
   188  		}
   189  		if !isManagementConflictPolicyValidForResource(policy, supportLeasing) {
   190  			return ManagementConflictPreventionPolicyNone, nil
   191  		}
   192  		return policy, nil
   193  	}
   194  	// if there is no value on the namespace return the default
   195  	return getDefaultManagementConflictPolicyForResource(supportLeasing), nil
   196  }
   197  
   198  func isManagementConflictPolicyValidForResource(policy ManagementConflictPreventionPolicy, supportLeasing bool) bool {
   199  	switch policy {
   200  	case ManagementConflictPreventionPolicyNone:
   201  		return true
   202  	case ManagementConflictPreventionPolicyResource:
   203  		return supportLeasing
   204  	default:
   205  		return false
   206  	}
   207  }
   208  
   209  // getDefaultManagementConflictPolicyForResource returns the default policy for a resource.
   210  //
   211  // This value was set to default to None, due to user complaints that label leasing behavior results
   212  // in resources sporadically setting not Ready, and causing issues for kpt live apply for a large
   213  // amount of resources.
   214  //
   215  // Before the default is flipped again, the label leaser should no longer flip the Ready state to false
   216  // and mark the resource as updating (https://github.com/GoogleCloudPlatform/k8s-config-connector/issues/387)
   217  func getDefaultManagementConflictPolicyForResource(supportLeasing bool) ManagementConflictPreventionPolicy {
   218  	return ManagementConflictPreventionPolicyNone
   219  }
   220  
   221  func validateManagementConflictPolicyForResource(policy ManagementConflictPreventionPolicy, supportLeasing bool) error {
   222  	switch policy {
   223  	case ManagementConflictPreventionPolicyNone:
   224  		return nil
   225  	case ManagementConflictPreventionPolicyResource:
   226  		if !supportLeasing {
   227  			return fmt.Errorf("the resource kind does not support usage of %v of '%v'",
   228  				ManagementConflictPreventionPolicyAnnotation, policy)
   229  		}
   230  		return nil
   231  	default:
   232  		return fmt.Errorf("unknown management conflict policy: %v", policy)
   233  	}
   234  }
   235  
   236  func valueToManagementConflictPreventionPolicy(value string) (ManagementConflictPreventionPolicy, error) {
   237  	for _, policy := range ManagementConflictPreventionPolicyValues {
   238  		if value == string(policy) {
   239  			return ManagementConflictPreventionPolicy(value), nil
   240  		}
   241  	}
   242  	return ManagementConflictPreventionPolicyNone, fmt.Errorf("invalid management conflict policy '%v', can be one of {%v}",
   243  		value, strings.Join(ManagementConflictPreventionPolicyValues, ", "))
   244  }
   245  
   246  func SetDefaultContainerAnnotation(obj metav1.Object, ns *corev1.Namespace, containers []corekccv1alpha1.Container) error {
   247  	if len(containers) == 0 {
   248  		// No defaulting required
   249  		return nil
   250  	}
   251  	// If the resource already has a container annotation, no modification is required
   252  	val, _, err := GetContainerAnnotation(obj.GetAnnotations(), ContainerTypes(containers))
   253  	if err != nil {
   254  		return fmt.Errorf("error getting container annotation from object: %v", err)
   255  	}
   256  	if val != "" {
   257  		return nil
   258  	}
   259  	// if the Namespace has a container annotation, we'll use that as the default
   260  	val, containerType, err := GetContainerAnnotation(ns.GetAnnotations(), ContainerTypes(containers))
   261  	if err != nil {
   262  		return fmt.Errorf("error getting container annotation from object: %v", err)
   263  	}
   264  	if val != "" {
   265  		SetAnnotation(GetAnnotationForContainerType(containerType), val, obj)
   266  		return nil
   267  	}
   268  	// For project-scoped resources we can use the namespace name as the project ID
   269  	if IsProjectScoped(containers) {
   270  		SetAnnotation(ProjectIDAnnotation, ns.GetName(), obj)
   271  		return nil
   272  	}
   273  	possibleAnnotations := containerTypesToAnnotations(ContainerTypes(containers))
   274  	return fmt.Errorf("neither resource nor namespace have the required container object annotation, one of: [%v]", strings.Join(possibleAnnotations, ", "))
   275  }
   276  
   277  // GetContainerAnnotation will get the appropriate container annotation from the given
   278  // annotations.
   279  func GetContainerAnnotation(annotations map[string]string, containerTypes []corekccv1alpha1.ContainerType) (string, corekccv1alpha1.ContainerType, error) {
   280  	var containerVal string
   281  	var containerType corekccv1alpha1.ContainerType
   282  	var found bool
   283  	for _, c := range containerTypes {
   284  		val, ok := annotations[GetAnnotationForContainerType(c)]
   285  		if !ok {
   286  			continue
   287  		}
   288  		if found {
   289  			return "", "", fmt.Errorf("ambiguious container annotation: found for %v and %v", containerType, c)
   290  		}
   291  		containerVal = val
   292  		containerType = c
   293  		found = true
   294  	}
   295  	return containerVal, containerType, nil
   296  }
   297  
   298  func IsProjectScoped(containers []corekccv1alpha1.Container) bool {
   299  	for _, c := range containers {
   300  		if c.Type == corekccv1alpha1.ContainerTypeProject {
   301  			return true
   302  		}
   303  	}
   304  	return false
   305  }
   306  
   307  func GetAnnotationForContainerType(containerType corekccv1alpha1.ContainerType) string {
   308  	switch containerType {
   309  	case corekccv1alpha1.ContainerTypeProject:
   310  		return ProjectIDAnnotation
   311  	case corekccv1alpha1.ContainerTypeFolder:
   312  		return FolderIDAnnotation
   313  	case corekccv1alpha1.ContainerTypeOrganization:
   314  		return OrgIDAnnotation
   315  	default:
   316  		panic(fmt.Errorf("unrecognized container type %v", containerType))
   317  	}
   318  }
   319  
   320  func containerTypesToAnnotations(containerTypes []corekccv1alpha1.ContainerType) []string {
   321  	annotations := make([]string, 0)
   322  	for _, c := range containerTypes {
   323  		annotations = append(annotations, GetAnnotationForContainerType(c))
   324  	}
   325  	return annotations
   326  }
   327  
   328  func ContainerTypes(containers []corekccv1alpha1.Container) []corekccv1alpha1.ContainerType {
   329  	types := make([]corekccv1alpha1.ContainerType, 0)
   330  	for _, c := range containers {
   331  		types = append(types, c.Type)
   332  	}
   333  	return types
   334  }
   335  
   336  // TriggerManagedFieldsMetadata ensures that managed fields metadata is present on the given
   337  // resource for Server-Side Apply (SSA) compatible clusters.
   338  func TriggerManagedFieldsMetadata(ctx context.Context, c client.Client, u *unstructured.Unstructured) (
   339  	*unstructured.Unstructured, error) {
   340  	if len(u.GetManagedFields()) > 0 {
   341  		// Managed fields metadata is present already; no action necessary.
   342  		return u, nil
   343  	}
   344  	// Attempt an SSA patch to trigger the initial SSA metadata on the resource. Construct an
   345  	// unstructured object that only specified the information we care about: a temporary SSA
   346  	// annotation in the annotations map.
   347  	patchSkeleton := &unstructured.Unstructured{}
   348  	patchSkeleton.SetGroupVersionKind(u.GroupVersionKind())
   349  	patchSkeleton.SetName(u.GetName())
   350  	patchSkeleton.SetNamespace(u.GetNamespace())
   351  
   352  	patchU := patchSkeleton.DeepCopy()
   353  	patchU.SetAnnotations(map[string]string{SupportsSSAAnnotation: "true"})
   354  	if err := c.Patch(ctx, patchU, client.Apply, client.FieldOwner(SupportsSSAManager)); err != nil {
   355  		if strings.Contains(err.Error(), string(types.MergePatchType)) {
   356  			// The patch was rejected due to the API server not supporting the Apply patch type.
   357  			// No action required.
   358  			return u, nil
   359  		}
   360  		return nil, fmt.Errorf("error patching SSA metadata annotation: %w", err)
   361  	}
   362  	// Now that the SSA metadata has been triggered, remove the annotation. The SSA metadata
   363  	// will persist.
   364  	patchU = patchSkeleton.DeepCopy()
   365  	if err := c.Patch(ctx, patchU, client.Apply, client.FieldOwner(SupportsSSAManager)); err != nil {
   366  		return nil, fmt.Errorf("error removing SSA metadata annotation: %w", err)
   367  	}
   368  	return patchU, nil
   369  }
   370  
   371  // KindWithoutServicePrefix returns the kind without the
   372  // service prefix (e.g. "ComputeBackendBucket => "BackendBucket").
   373  // Kinds which do not contain a service prefix are returned directly
   374  // (e.g.  "Project" => "Project").
   375  func KindWithoutServicePrefix(gvk schema.GroupVersionKind) string {
   376  	switch gvk.Kind {
   377  	case "Project", "Folder", "Organization", "BillingAccount":
   378  		// Some kinds do not contain a service prefix.
   379  		return gvk.Kind
   380  	default:
   381  		serviceInLowerCase := strings.TrimSuffix(gvk.Group, "."+CNRMGroup)
   382  		kindInLowerCase := strings.ToLower(gvk.Kind)
   383  		if !strings.HasPrefix(kindInLowerCase, serviceInLowerCase) {
   384  			panic(fmt.Errorf("kind %v unexpectedly does not begin with its service name", gvk.Kind))
   385  		}
   386  		return gvk.Kind[len(serviceInLowerCase):]
   387  	}
   388  }
   389  

View as plain text