1 // Copyright 2020 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package valueadd 5 6 import ( 7 "strings" 8 9 "sigs.k8s.io/kustomize/kyaml/filesys" 10 "sigs.k8s.io/kustomize/kyaml/kio" 11 "sigs.k8s.io/kustomize/kyaml/yaml" 12 ) 13 14 // An 'Add' operation aspiring to IETF RFC 6902 JSON. 15 // 16 // The filter tries to add a value to a node at a particular field path. 17 // 18 // Kinds of target fields: 19 // 20 // - Non-existent target field. 21 // 22 // The field will be added and the value inserted. 23 // 24 // - Existing field, scalar or map. 25 // 26 // E.g. 'spec/template/spec/containers/[name:nginx]/image' 27 // 28 // This behaves like an IETF RFC 6902 Replace operation would; 29 // the existing value is replaced without complaint, even though 30 // this is an Add operation. In contrast, a Replace operation 31 // must fail (report an error) if the field doesn't exist. 32 // 33 // - Existing field, list (array) 34 // Not supported yet. 35 // TODO: Honor fields with RFC-6902-style array indices 36 // TODO: like 'spec/template/spec/containers/2' 37 // TODO: Modify kyaml/yaml/PathGetter to allow this. 38 // The value will be inserted into the array at the given position, 39 // shifting other contents. To instead replace an array entry, use 40 // an implementation of an IETF RFC 6902 Replace operation. 41 // 42 // For the common case of a filepath in the field value, and a desire 43 // to add the value to the filepath (rather than replace the filepath), 44 // use a non-zero value of FilePathPosition (see below). 45 type Filter struct { 46 // Value is the value to add. 47 // 48 // Empty values are disallowed, i.e. this filter isn't intended 49 // for use in erasing or removing fields. For that, use a filter 50 // more aligned with the IETF RFC 6902 JSON Remove operation. 51 // 52 // At the time of writing, Value's value should be a simple string, 53 // not a JSON document. This particular filter focuses on easing 54 // injection of a single-sourced cloud project and/or cluster name 55 // into various fields, especially namespace and various filepath 56 // specifications. 57 Value string 58 59 // FieldPath is a JSON-style path to the field intended to hold the value. 60 FieldPath string 61 62 // FilePathPosition is a filepath field index. 63 // 64 // Call the value of this field _i_. 65 // 66 // If _i_ is zero, negative or unspecified, this field has no effect. 67 // 68 // If _i_ is > 0, then it's assumed that 69 // - 'Value' is a string that can work as a directory or file name, 70 // - the field value intended for replacement holds a filepath. 71 // 72 // The filepath is split into a string slice, the value is inserted 73 // at position [i-1], shifting the rest of the path to the right. 74 // A value of i==1 puts the new value at the start of the path. 75 // This change never converts an absolute path to a relative path, 76 // meaning adding a new field at position i==1 will preserve a 77 // leading slash. E.g. if Value == 'PEACH' 78 // 79 // OLD : NEW : FilePathPosition 80 // -------------------------------------------------------- 81 // {empty} : PEACH : irrelevant 82 // / : /PEACH : irrelevant 83 // pie : PEACH/pie : 1 (or less to prefix) 84 // /pie : /PEACH/pie : 1 (or less to prefix) 85 // raw : raw/PEACH : 2 (or more to postfix) 86 // /raw : /raw/PEACH : 2 (or more to postfix) 87 // a/nice/warm/pie : a/nice/warm/PEACH/pie : 4 88 // /a/nice/warm/pie : /a/nice/warm/PEACH/pie : 4 89 // 90 // For robustness (liberal input, conservative output) FilePathPosition 91 // values that that are too large to index the split filepath result in a 92 // postfix rather than an error. So use 1 to prefix, 9999 to postfix. 93 FilePathPosition int `json:"filePathPosition,omitempty" yaml:"filePathPosition,omitempty"` 94 } 95 96 var _ kio.Filter = Filter{} 97 98 func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { 99 _, err := kio.FilterAll(yaml.FilterFunc( 100 func(node *yaml.RNode) (*yaml.RNode, error) { 101 var fields []string 102 // if there is forward slash '/' in the field name, a back slash '\' 103 // will be used to escape it. 104 for _, f := range strings.Split(f.FieldPath, "/") { 105 if len(fields) > 0 && strings.HasSuffix(fields[len(fields)-1], "\\") { 106 concatField := strings.TrimSuffix(fields[len(fields)-1], "\\") + "/" + f 107 fields = append(fields[:len(fields)-1], concatField) 108 } else { 109 fields = append(fields, f) 110 } 111 } 112 // TODO: support SequenceNode. 113 // Presumably here one could look for array indices (digits) at 114 // the end of the field path (as described in IETF RFC 6902 JSON), 115 // and if found, take it as a signal that this should be a 116 // SequenceNode instead of a ScalarNode, and insert the value 117 // into the proper slot, shifting every over. 118 n, err := node.Pipe(yaml.LookupCreate(yaml.ScalarNode, fields...)) 119 if err != nil { 120 return node, err 121 } 122 // TODO: allow more kinds 123 if err := yaml.ErrorIfInvalid(n, yaml.ScalarNode); err != nil { 124 return nil, err 125 } 126 newValue := f.Value 127 if f.FilePathPosition > 0 { 128 newValue = filesys.InsertPathPart( 129 n.YNode().Value, f.FilePathPosition-1, newValue) 130 } 131 return n.Pipe(yaml.FieldSetter{StringValue: newValue}) 132 })).Filter(nodes) 133 return nodes, err 134 } 135