...

Source file src/github.com/datawire/ambassador/v2/pkg/snapshot/v1/annotations.go

Documentation: github.com/datawire/ambassador/v2/pkg/snapshot/v1

     1  package snapshot
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	crdAll "github.com/datawire/ambassador/v2/pkg/api/getambassador.io"
     9  	crdCurrent "github.com/datawire/ambassador/v2/pkg/api/getambassador.io/v3alpha1"
    10  	"github.com/datawire/ambassador/v2/pkg/kates"
    11  	"github.com/datawire/dlib/derror"
    12  )
    13  
    14  func annotationKey(obj kates.Object) string {
    15  	return fmt.Sprintf("%s/%s.%s",
    16  		obj.GetObjectKind().GroupVersionKind().Kind,
    17  		obj.GetName(),
    18  		obj.GetNamespace())
    19  }
    20  
    21  var (
    22  	scheme    = crdAll.BuildScheme()
    23  	validator = crdAll.NewValidator()
    24  )
    25  
    26  func (s *KubernetesSnapshot) PopulateAnnotations(ctx context.Context) error {
    27  	var annotatable []kates.Object
    28  	for _, svc := range s.Services {
    29  		annotatable = append(annotatable, svc)
    30  	}
    31  	for _, ing := range s.Ingresses {
    32  		annotatable = append(annotatable, ing)
    33  	}
    34  
    35  	s.Annotations = make(map[string]AnnotationList)
    36  	var errs derror.MultiError
    37  	for _, r := range annotatable {
    38  		key := annotationKey(r)
    39  		objs, err := ParseAnnotationResources(r)
    40  		if err != nil {
    41  			errs = append(errs, fmt.Errorf("%s: %w", key, err))
    42  			continue
    43  		}
    44  		annotations := make(AnnotationList, len(objs))
    45  		for i, untypedObj := range objs {
    46  			typedObj, err := ValidateAndConvertObject(ctx, untypedObj)
    47  			if err != nil {
    48  				untypedObj.Object["errors"] = err.Error()
    49  				annotations[i] = untypedObj
    50  			} else {
    51  				annotations[i] = typedObj
    52  			}
    53  		}
    54  		if len(annotations) > 0 {
    55  			s.Annotations[key] = annotations
    56  		}
    57  	}
    58  
    59  	if len(errs) > 0 {
    60  		return errs
    61  	}
    62  	return nil
    63  }
    64  
    65  // ValidateAndConvertObject validates an apiGroup=getambassador.io resource, and converts it to the
    66  // preferred version.
    67  //
    68  // This is meant for use on objects that come from annotations.  You should probably not be calling
    69  // this directly; the only reason it's public is for use by tests.
    70  func ValidateAndConvertObject(
    71  	ctx context.Context,
    72  	in *kates.Unstructured,
    73  ) (out kates.Object, err error) {
    74  	// Validate it
    75  	gvk := in.GetObjectKind().GroupVersionKind()
    76  	if !scheme.Recognizes(gvk) {
    77  		return nil, fmt.Errorf("unsupported GroupVersionKind %q, ignoring", gvk)
    78  	}
    79  	if err := validator.Validate(ctx, in); err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	// Convert it to the correct type+version.
    84  	out, err = convertAnnotationObject(in)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	// Validate it again (after conversion) just to be safe
    90  	if err := validator.Validate(ctx, out); err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	return out, nil
    95  }
    96  
    97  // convertAnnotationObject converts a valid kates.Object to the correct type+version.
    98  func convertAnnotationObject(srcUnstruct *kates.Unstructured) (kates.Object, error) {
    99  	// Convert from an 'Unstructured' to the appropriate Go type, without actually converting
   100  	// versions.
   101  	srcGVK := srcUnstruct.GetObjectKind().GroupVersionKind()
   102  	_src, err := scheme.ConvertToVersion(srcUnstruct, srcGVK.GroupVersion())
   103  	if err != nil {
   104  		return nil, fmt.Errorf("1: %w", err)
   105  	}
   106  	src, ok := _src.(kates.Object)
   107  	if !ok {
   108  		return nil, fmt.Errorf("type %T doesn't implement kates.Object", _src)
   109  	}
   110  
   111  	// Create the Go type of the output version.
   112  	dstGVK := crdCurrent.GroupVersion.WithKind(srcGVK.Kind)
   113  	if dstGVK == srcGVK {
   114  		// Optimize!  Plus, kates.ConvertObject doesn't like doing no-op conversions.
   115  		return src, nil
   116  	}
   117  	_dst, err := scheme.New(dstGVK)
   118  	if err != nil {
   119  		return nil, fmt.Errorf("2: %w", err)
   120  	}
   121  	dst, ok := _dst.(kates.Object)
   122  	if !ok {
   123  		return nil, fmt.Errorf("type %T doesn't implement kates.Object", _dst)
   124  	}
   125  	dst.GetObjectKind().SetGroupVersionKind(dstGVK)
   126  
   127  	// Convert versions.  This is based around Go types, which is why we need to convert from
   128  	// 'Unstructured' first.
   129  	if err := kates.ConvertObject(scheme, src, dst); err != nil {
   130  		return nil, fmt.Errorf("3: %w", err)
   131  	}
   132  	return dst, nil
   133  }
   134  
   135  // ParseAnnotationResources parses the annotations on an object, and munges them to be
   136  // Kubernetes-structured objects.  It does not do any validation or version conversion.
   137  //
   138  // You should probably not be calling this directly; the only reason it's public is for use by
   139  // tests.
   140  func ParseAnnotationResources(resource kates.Object) ([]*kates.Unstructured, error) {
   141  	annotationStr, annotationStrOK := resource.GetAnnotations()["getambassador.io/config"]
   142  	if !annotationStrOK {
   143  		return nil, nil
   144  	}
   145  	// Parse in to a scratch _annotationResources list instead of the final annotationResources, so that we can more
   146  	// easily prune invalid entries out before returning it.
   147  	_annotationResources, err := kates.ParseManifestsToUnstructured(annotationStr)
   148  	if err != nil {
   149  		return nil, fmt.Errorf("annotation getambassador.io/config: could not parse YAML: %w", err)
   150  	}
   151  	annotationResources := make([]*kates.Unstructured, 0, len(_annotationResources))
   152  	for _, _annotationResource := range _annotationResources {
   153  		annotationResource := _annotationResource.(*kates.Unstructured).Object
   154  		// Un-fold annotations with collapsed metadata/spec
   155  		if _, ok := annotationResource["apiVersion"].(string); !ok {
   156  			annotationResource["apiVersion"] = ""
   157  		}
   158  		if dat, ok := annotationResource["metadata"].(map[string]interface{}); !ok || dat == nil {
   159  			annotationResource["metadata"] = map[string]interface{}{}
   160  		}
   161  		if dat, ok := annotationResource["spec"].(map[string]interface{}); !ok || dat == nil {
   162  			annotationResource["spec"] = map[string]interface{}{}
   163  		}
   164  		for k, v := range annotationResource {
   165  			switch k {
   166  			case "apiVersion", "kind", "metadata", "spec", "status":
   167  				// do nothing
   168  			case "name", "namespace", "generation":
   169  				annotationResource["metadata"].(map[string]interface{})[k] = v
   170  				delete(annotationResource, k)
   171  			case "metadata_labels":
   172  				annotationResource["metadata"].(map[string]interface{})["labels"] = v
   173  				delete(annotationResource, k)
   174  			default:
   175  				annotationResource["spec"].(map[string]interface{})[k] = v
   176  				delete(annotationResource, k)
   177  			}
   178  		}
   179  
   180  		// Default attributes from the parent
   181  		if ns, ok := annotationResource["metadata"].(map[string]interface{})["namespace"].(string); !ok || ns == "" {
   182  			annotationResource["metadata"].(map[string]interface{})["namespace"] = resource.GetNamespace()
   183  		}
   184  		if annotationResource["metadata"].(map[string]interface{})["labels"] == nil && resource.GetLabels() != nil {
   185  			annotationResource["metadata"].(map[string]interface{})["labels"] = resource.GetLabels()
   186  		}
   187  
   188  		// The Canonical API Version for our resources always starts with "getambassador.io/",
   189  		// but it used to always start with "ambassador/". Translate as needed for backward
   190  		// compatibility.
   191  		if apiVersion := annotationResource["apiVersion"].(string); strings.HasPrefix(apiVersion, "ambassador/") {
   192  			annotationResource["apiVersion"] = "getambassador.io/" + strings.TrimPrefix(apiVersion, "ambassador/")
   193  		}
   194  
   195  		// Add it to the snapshot
   196  		annotationResources = append(annotationResources, &kates.Unstructured{Object: annotationResource})
   197  	}
   198  	return annotationResources, nil
   199  }
   200  

View as plain text