...

Source file src/edge-infra.dev/pkg/edge/gitops/fns/edgerelease/edgerelease.go

Documentation: edge-infra.dev/pkg/edge/gitops/fns/edgerelease

     1  // Package edgerelease implements the EdgeRelease Kpt function, based on the
     2  // Kpt delivery pipeline RFC: https://docs.edge-infra.dev/rfc/kpt-delivery-pipeline/.
     3  // This function can be used to update container image digests for GKE component
     4  // manifests as well as GKE Kpt functions.
     5  //
     6  // Current limitations:
     7  //
     8  // - Cannot handle multiple-image components
     9  package edgerelease
    10  
    11  import (
    12  	"fmt"
    13  	"strings"
    14  
    15  	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
    16  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    17  	"sigs.k8s.io/kustomize/kyaml/yaml"
    18  
    19  	"edge-infra.dev/pkg/edge/component/build"
    20  	"edge-infra.dev/pkg/edge/component/build/image"
    21  	"edge-infra.dev/pkg/edge/constants"
    22  	fnv1alpha1 "edge-infra.dev/pkg/edge/gitops/fn/v1alpha1"
    23  	"edge-infra.dev/pkg/k8s/meta"
    24  )
    25  
    26  // containerPaths are the subpaths within the PodSpec that contain ContainerSpecs
    27  var containerPaths = []string{"containers", "initContainers"}
    28  
    29  // Run accepts an input array of yaml.RNodes and updates the resources with the
    30  // configured build metadata + image digest if it corresponds to one of the
    31  // provided component or Kpt function names.
    32  //
    33  // Component manifests are identified by checking
    34  // the provided component name against the value of the
    35  // `platform.edge.ncr.com/component` label.
    36  //
    37  // Kpt functions are identified by their apiVersion (e.g., fns.edge.ncr.com/v1alpha1)
    38  // and the image name in their function configuration annotation.  If the image
    39  // name is present in the list of provided Kpt function images, it is updated
    40  // with the provided digest.
    41  //
    42  // TODO: handle multiple-image Components
    43  func (f *EdgeRelease) Run(input []*yaml.RNode) ([]*yaml.RNode, error) {
    44  	if err := f.Spec.Validate(); err != nil {
    45  		return nil, err
    46  	}
    47  
    48  	f.init()
    49  
    50  	input, err := kioutil.MapMeta(input, f.componentMapper)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	input, err = kioutil.MapMeta(input, f.kptFnMapper)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	return input, nil
    61  }
    62  
    63  func (f *EdgeRelease) componentMapper(i *yaml.RNode, m yaml.ResourceMeta) (*yaml.RNode, error) {
    64  	// determine if this resource belongs to a platform component
    65  	cname, ok := m.Labels[constants.PlatformComponent]
    66  	if !ok {
    67  		return i, nil
    68  	}
    69  
    70  	// check if the component is one we are updating
    71  	componentImgs, ok := f.componentMap[cname]
    72  	// we only want to update manifests which describe workloads for our component
    73  	if !ok || !meta.IsWorkload(m.Kind) {
    74  		return i, nil
    75  	}
    76  
    77  	// add build metadata labels
    78  	if f.Spec.AddBuildLabels {
    79  		if err := f.addBuildLabels(i, m.Labels); err != nil {
    80  			return nil, wrapErr("edgerelease.componentMapper: failed to add build labels", m, err)
    81  		}
    82  	}
    83  
    84  	// grab the correct object path for the []ContainerSpec
    85  	podSpecPath, err := getPodSpecPath(m.Kind)
    86  	if err != nil {
    87  		return nil, wrapErr("edgerelease.componentMapper: failed to determine PodSpec path from workload", m, err)
    88  	}
    89  
    90  	// Single anonymous image components cannot support initContainers, so
    91  	// just run the "containers" path and return
    92  	if isDefaultImg(componentImgs) {
    93  		if err := patchContainers(i, componentImgs, append(podSpecPath, "containers")); err != nil {
    94  			return nil, wrapErr("edgerelease.componentMapper: failed to patch containers", m, err)
    95  		}
    96  		return i, nil
    97  	}
    98  
    99  	for _, p := range containerPaths {
   100  		if err := patchContainers(i, componentImgs, append(podSpecPath, p)); err != nil {
   101  			return nil, wrapErr("edgerelease.componentMapper: failed to patch containers", m, err)
   102  		}
   103  	}
   104  
   105  	return i, nil
   106  }
   107  
   108  func patchContainers(i *yaml.RNode, imgs map[string]string, path []string) error {
   109  	// get list of containers
   110  	containerList, err := i.Pipe(yaml.Lookup(path...))
   111  	if err != nil {
   112  		return fmt.Errorf("failed to get list of containers from workload: %w", err)
   113  	}
   114  
   115  	// turn it into an array so we can work with it more easily
   116  	containers, err := containerList.Elements()
   117  	if err != nil {
   118  		return fmt.Errorf("failed to get list of containers from workload: %w", err)
   119  	}
   120  
   121  	// if there is only one container and we only have one image, we can
   122  	// simply replace the reference.  this is kept here for backwards compatibility
   123  	// with single image workloads that might not provide a container name
   124  	// it should probably be removed long-term once users of EdgeRelease with
   125  	// single container workloads have updated their usage
   126  	// only match this if the default image name is detected
   127  	if isDefaultImg(imgs) && len(containers) == 1 {
   128  		newImg := yaml.NewStringRNode(imgs[defaultImgName])
   129  		return containers[0].PipeE(yaml.SetField("image", newImg))
   130  	}
   131  
   132  	for i := range containers {
   133  		c := containers[i]
   134  		name, err := c.Pipe(yaml.Lookup("name"))
   135  		if err != nil {
   136  			return err
   137  		}
   138  
   139  		if ref, ok := imgs[strings.TrimSpace(name.MustString())]; ok {
   140  			if err := c.PipeE(yaml.SetField("image", yaml.NewStringRNode(ref))); err != nil {
   141  				return err
   142  			}
   143  		}
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  func (f *EdgeRelease) kptFnMapper(i *yaml.RNode, m yaml.ResourceMeta) (*yaml.RNode, error) {
   150  	// check if its an GKE Kpt function
   151  	if m.APIVersion != fnv1alpha1.GroupVersion.String() {
   152  		return i, nil
   153  	}
   154  
   155  	fnAnno, ok := m.Annotations[runtimeutil.FunctionAnnotationKey]
   156  	// no function config, we can't update anything
   157  	if !ok {
   158  		return i, nil
   159  	}
   160  
   161  	fnConfig, err := yaml.Parse(fnAnno)
   162  	if err != nil {
   163  		return nil, wrapErr("edgerelease.kptFnMapper: failed to parse function annotation", m, err)
   164  	}
   165  
   166  	imgNode, err := fnConfig.Pipe(yaml.Lookup("container", "image"))
   167  	if err != nil {
   168  		return nil, wrapErr("edgerelease.kptFnMapper: failed to look up image in function annotation", m, err)
   169  	}
   170  
   171  	// if there was nothing found at `container.image` in the Kpt function anno,
   172  	// this function doesn't rely on docker runtime and we can skip it
   173  	if imgNode == nil {
   174  		return i, nil
   175  	}
   176  
   177  	img, err := imgNode.String()
   178  	if err != nil {
   179  		return nil, wrapErr("edgerelease.kptFnMapper: failed to get string for function image", m, err)
   180  	}
   181  
   182  	kptFn, ok := f.kptFnMap[image.NameFromRef(img)]
   183  	if !ok {
   184  		return i, nil
   185  	}
   186  
   187  	if f.Spec.AddBuildLabels {
   188  		if err := f.addBuildLabels(i, m.Labels); err != nil {
   189  			return nil, wrapErr("edgerelease.kptFnMapper: failed to add build labels", m, err)
   190  		}
   191  	}
   192  
   193  	newImg := map[string]string{"image": kptFn.Reference()}
   194  	if err := fnConfig.PipeE(yaml.SetField("container", yaml.NewMapRNode(&newImg))); err != nil {
   195  		return nil, wrapErr("edgerelease.kptFnMapper: failed to update function annotation", m, err)
   196  	}
   197  
   198  	fnConfigStr, err := fnConfig.String()
   199  	if err != nil {
   200  		return nil, wrapErr("edgerelease.kptFnMapper: failed to convert new function annotation to string", m, err)
   201  	}
   202  
   203  	if err := i.PipeE(yaml.SetAnnotation(runtimeutil.FunctionAnnotationKey, fnConfigStr)); err != nil {
   204  		return nil, wrapErr("edgerelease.kptFnMapper: failed to set function annotation", m, err)
   205  	}
   206  
   207  	return i, nil
   208  }
   209  
   210  // addBuildLabels adds the build metadata from an EdgeRelease struct to an existing
   211  // label map
   212  func (f *EdgeRelease) addBuildLabels(i *yaml.RNode, labels map[string]string) error {
   213  	if labels == nil {
   214  		labels = map[string]string{}
   215  	}
   216  	labels[build.CommitLabel] = f.Spec.Metadata.Commit
   217  	labels[build.SemVerLabel] = f.Spec.Metadata.SemVer
   218  	labels[build.TimestampLabel] = f.Spec.Metadata.Timestamp
   219  	labels[build.BuildIDLabel] = f.Spec.Metadata.ID
   220  	labels[build.RepoLabel] = f.Spec.Metadata.Repo
   221  	labels[build.OrgLabel] = f.Spec.Metadata.Org
   222  
   223  	return i.SetLabels(labels)
   224  }
   225  
   226  func getPodSpecPath(kind string) ([]string, error) {
   227  	switch kind {
   228  	// all workload types other than CronJobs have the same  path to the pod spec
   229  	case "Deployment", "DaemonSet", "StatefulSet", "Job":
   230  		return []string{"spec", "template", "spec"}, nil
   231  	case "CronJob":
   232  		return []string{"spec", "jobTemplate", "spec", "template", "spec"}, nil
   233  	default:
   234  		return []string{}, fmt.Errorf("could not match %s when looking up PodSpec path", kind)
   235  	}
   236  }
   237  
   238  func wrapErr(msg string, m yaml.ResourceMeta, err error) error {
   239  	if path, ok := m.Annotations[kioutil.PathAnnotation]; ok {
   240  		return fmt.Errorf("%s: %s/%s from %s. error: %w", msg, m.Kind, m.Name, path, err)
   241  	}
   242  	return fmt.Errorf("%s: %s/%s. error: %w", msg, m.Kind, m.Name, err)
   243  }
   244  
   245  // isDefaultImg detects if the current image map input is the base case:
   246  // a single image named defaultImage
   247  func isDefaultImg(imgs map[string]string) bool {
   248  	_, ok := imgs[defaultImgName]
   249  	return ok && len(imgs) == 1
   250  }
   251  

View as plain text