github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/fnruntime/utils.go (about)

     1  // Copyright 2021 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 fnruntime
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"path/filepath"
    21  
    22  	"github.com/GoogleContainerTools/kpt/internal/types"
    23  	fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1"
    24  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    25  	"sigs.k8s.io/kustomize/kyaml/filesys"
    26  	"sigs.k8s.io/kustomize/kyaml/yaml"
    27  )
    28  
    29  // ResourceIDAnnotation is used to uniquely identify the resource during round trip
    30  // to and from a function execution. This annotation is meant to be consumed by
    31  // kpt during round trip and should be deleted after that
    32  const ResourceIDAnnotation = "internal.config.k8s.io/kpt-resource-id"
    33  
    34  // SaveResults saves results gathered from running the pipeline at specified dir in the input FileSystem.
    35  func SaveResults(fsys filesys.FileSystem, resultsDir string, fnResults *fnresult.ResultList) (string, error) {
    36  	if resultsDir == "" {
    37  		return "", nil
    38  	}
    39  	filePath := filepath.Join(resultsDir, "results.yaml")
    40  	out := &bytes.Buffer{}
    41  
    42  	// use kyaml encoder to ensure consistent indentation
    43  	e := yaml.NewEncoderWithOptions(out, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle})
    44  	err := e.Encode(fnResults)
    45  	if err != nil {
    46  		return "", err
    47  	}
    48  
    49  	err = fsys.WriteFile(filePath, out.Bytes())
    50  	if err != nil {
    51  		return "", err
    52  	}
    53  
    54  	return filePath, nil
    55  }
    56  
    57  // MergeWithInput merges the transformed output with input resources
    58  // input: all input resources, selectedInput: selected input resources
    59  // output: output resources as the result of function on selectedInput resources
    60  // for input: A,B,C,D; selectedInput: A,B; output: A,E(A transformed, B Deleted, E Added)
    61  // the result should be A,C,D,E
    62  // resources are identified by kpt-resource-id annotation
    63  func MergeWithInput(output, selectedInput, input []*yaml.RNode) []*yaml.RNode {
    64  	var result []*yaml.RNode
    65  	for i := range input {
    66  		presentInSelectedInput := presentIn(input[i], selectedInput)
    67  		presentInOutput := presentIn(input[i], output)
    68  		if !presentInSelectedInput {
    69  			// this resource is untouched
    70  			result = append(result, input[i])
    71  			continue
    72  		}
    73  
    74  		if presentInOutput {
    75  			// this resource modified by function, so replace it from the output
    76  			result = append(result, nodeWithResourceID(input[i].GetAnnotations()[ResourceIDAnnotation], output))
    77  		}
    78  		// if presentInSelectedInput and !presentInOutput the resource is deleted, so ignore it
    79  	}
    80  
    81  	// add function generated resources to the result
    82  	for i := range output {
    83  		if output[i].GetAnnotations()[ResourceIDAnnotation] == "" {
    84  			result = append(result, output[i])
    85  		}
    86  	}
    87  
    88  	return result
    89  }
    90  
    91  // nodeWithResourceID returns the node with the input resourceId
    92  func nodeWithResourceID(resourceID string, input []*yaml.RNode) *yaml.RNode {
    93  	for _, node := range input {
    94  		if node.GetAnnotations()[ResourceIDAnnotation] == resourceID {
    95  			return node
    96  		}
    97  	}
    98  	return nil
    99  }
   100  
   101  // presentIn returns true if the targetNode identified by kpt-resource-id annotation
   102  // is present in the input list of resources
   103  func presentIn(targetNode *yaml.RNode, input []*yaml.RNode) bool {
   104  	id := targetNode.GetAnnotations()[ResourceIDAnnotation]
   105  	for _, node := range input {
   106  		if node.GetAnnotations()[ResourceIDAnnotation] == id {
   107  			return true
   108  		}
   109  	}
   110  	return false
   111  }
   112  
   113  // SetResourceIds adds kpt-resource-id annotation to each input resource
   114  func SetResourceIds(input []*yaml.RNode) error {
   115  	id := 0
   116  	for i := range input {
   117  		idStr := fmt.Sprintf("%v", id)
   118  		err := input[i].PipeE(yaml.SetAnnotation(ResourceIDAnnotation, idStr))
   119  		if err != nil {
   120  			return err
   121  		}
   122  		id++
   123  	}
   124  	return nil
   125  }
   126  
   127  // DeleteResourceIds removes the kpt-resource-id annotation from all resources
   128  func DeleteResourceIds(input []*yaml.RNode) error {
   129  	for i := range input {
   130  		err := input[i].PipeE(yaml.ClearAnnotation(ResourceIDAnnotation))
   131  		if err != nil {
   132  			return err
   133  		}
   134  	}
   135  	return nil
   136  }
   137  
   138  type SelectionContext struct {
   139  	RootPackagePath types.UniquePath
   140  }
   141  
   142  // SelectInput returns the selected resources based on criteria in selectors
   143  func SelectInput(input []*yaml.RNode, selectors, exclusions []kptfilev1.Selector, _ *SelectionContext) ([]*yaml.RNode, error) {
   144  	var selectedInput []*yaml.RNode
   145  	if len(selectors) == 0 {
   146  		selectedInput = input
   147  	} else {
   148  		for _, node := range input {
   149  			for _, selector := range selectors {
   150  				if isMatch(node, selector) {
   151  					selectedInput = append(selectedInput, node)
   152  				}
   153  			}
   154  		}
   155  	}
   156  	if len(exclusions) == 0 {
   157  		return selectedInput, nil
   158  	}
   159  	var filteredInput []*yaml.RNode
   160  	for _, node := range selectedInput {
   161  		matchesExclusion := false
   162  		for _, exclusion := range exclusions {
   163  			if !exclusion.IsEmpty() && isMatch(node, exclusion) {
   164  				matchesExclusion = true
   165  				break
   166  			}
   167  		}
   168  		if !matchesExclusion {
   169  			filteredInput = append(filteredInput, node)
   170  		}
   171  	}
   172  	return filteredInput, nil
   173  }
   174  
   175  // isMatch returns true if the resource matches input selection criteria
   176  func isMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   177  	// keep expanding with new selectors
   178  	return nameMatch(node, selector) && namespaceMatch(node, selector) &&
   179  		kindMatch(node, selector) && apiVersionMatch(node, selector) &&
   180  		labelMatch(node, selector) && annoMatch(node, selector)
   181  }
   182  
   183  // nameMatch returns true if the resource name matches input selection criteria
   184  func nameMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   185  	return selector.Name == "" || selector.Name == node.GetName()
   186  }
   187  
   188  // namespaceMatch returns true if the resource namespace matches input selection criteria
   189  func namespaceMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   190  	return selector.Namespace == "" || selector.Namespace == node.GetNamespace()
   191  }
   192  
   193  // kindMatch returns true if the resource kind matches input selection criteria
   194  func kindMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   195  	return selector.Kind == "" || selector.Kind == node.GetKind()
   196  }
   197  
   198  // apiVersionMatch returns true if the resource apiVersion matches input selection criteria
   199  func apiVersionMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   200  	return selector.APIVersion == "" || selector.APIVersion == node.GetApiVersion()
   201  }
   202  
   203  // labelMatch returns true if the resource labels match input selection criteria
   204  func labelMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   205  	nodeLabels := node.GetLabels()
   206  	for sk, sv := range selector.Labels {
   207  		if nv, found := nodeLabels[sk]; !found || sv != nv {
   208  			return false
   209  		}
   210  	}
   211  	return true
   212  }
   213  
   214  // annoMatch returns true if the resource annotations match input selection criteria
   215  func annoMatch(node *yaml.RNode, selector kptfilev1.Selector) bool {
   216  	nodeAnnos := node.GetAnnotations()
   217  	for sk, sv := range selector.Annotations {
   218  		if nv, found := nodeAnnos[sk]; !found || sv != nv {
   219  			return false
   220  		}
   221  	}
   222  	return true
   223  }
   224  
   225  func NewConfigMap(data map[string]string) (*yaml.RNode, error) {
   226  	node := yaml.NewMapRNode(&data)
   227  	if node == nil {
   228  		return nil, nil
   229  	}
   230  	// create a ConfigMap only for configMap config
   231  	configMap := yaml.MustParse(`
   232  apiVersion: v1
   233  kind: ConfigMap
   234  metadata:
   235    name: function-input
   236  data: {}
   237  `)
   238  	if err := node.VisitFields(func(node *yaml.MapNode) error {
   239  		v := node.Value.YNode()
   240  		v.Tag = yaml.NodeTagString
   241  		node.Value.SetYNode(v)
   242  		return nil
   243  	}); err != nil {
   244  		return nil, err
   245  	}
   246  	if err := configMap.PipeE(yaml.SetField("data", node)); err != nil {
   247  		return nil, err
   248  	}
   249  	return configMap, nil
   250  }