sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/yamlprocessor/simple_processor.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package yamlprocessor 18 19 import ( 20 "fmt" 21 "regexp" 22 "sort" 23 "strings" 24 25 "github.com/drone/envsubst/v2" 26 "github.com/drone/envsubst/v2/parse" 27 ) 28 29 // SimpleProcessor is a yaml processor that uses envsubst to substitute values 30 // for variables in the format ${var}. It also allows default values if 31 // specified in the format ${var:=default}. 32 // See https://github.com/drone/envsubst for more details. 33 type SimpleProcessor struct{} 34 35 var _ Processor = &SimpleProcessor{} 36 37 // NewSimpleProcessor returns a new simple template processor. 38 func NewSimpleProcessor() *SimpleProcessor { 39 return &SimpleProcessor{} 40 } 41 42 // GetTemplateName returns the name of the template that the simple processor 43 // uses. It follows the cluster template naming convention of 44 // "cluster-template<-flavor>.yaml". 45 func (tp *SimpleProcessor) GetTemplateName(_, flavor string) string { 46 name := "cluster-template" 47 if flavor != "" { 48 name = fmt.Sprintf("%s-%s", name, flavor) 49 } 50 name = fmt.Sprintf("%s.yaml", name) 51 52 return name 53 } 54 55 // GetClusterClassTemplateName returns the name of the cluster class template 56 // that the simple processor uses. It follows the cluster class template naming convention 57 // of "clusterclass<-name>.yaml". 58 func (tp *SimpleProcessor) GetClusterClassTemplateName(_, name string) string { 59 return fmt.Sprintf("clusterclass-%s.yaml", name) 60 } 61 62 // GetVariables returns a list of the variables specified in the yaml. 63 func (tp *SimpleProcessor) GetVariables(rawArtifact []byte) ([]string, error) { 64 variables, err := tp.GetVariableMap(rawArtifact) 65 if err != nil { 66 return nil, err 67 } 68 varNames := make([]string, 0, len(variables)) 69 for k := range variables { 70 varNames = append(varNames, k) 71 } 72 sort.Strings(varNames) 73 return varNames, nil 74 } 75 76 // GetVariableMap returns a map of the variables specified in the yaml. 77 func (tp *SimpleProcessor) GetVariableMap(rawArtifact []byte) (map[string]*string, error) { 78 strArtifact := convertLegacyVars(string(rawArtifact)) 79 variables, err := inspectVariables(strArtifact) 80 if err != nil { 81 return nil, err 82 } 83 varMap := make(map[string]*string, len(variables)) 84 for k, v := range variables { 85 if v == "" { 86 varMap[k] = nil 87 } else { 88 v := v 89 varMap[k] = &v 90 } 91 } 92 return varMap, nil 93 } 94 95 // Process returns the final yaml with all the variables replaced with their 96 // respective values. If there are variables without corresponding values, it 97 // will return the raw yaml along with an error. 98 func (tp *SimpleProcessor) Process(rawArtifact []byte, variablesClient func(string) (string, error)) ([]byte, error) { 99 tmp := convertLegacyVars(string(rawArtifact)) 100 // Inspect the yaml read from the repository for variables. 101 variables, err := inspectVariables(tmp) 102 if err != nil { 103 return rawArtifact, err 104 } 105 106 var missingVariables []string 107 // keep track of missing variables to return as error later 108 for name, defaultValue := range variables { 109 _, err := variablesClient(name) 110 // add to missingVariables list if the variable does not exist in the 111 // variablesClient AND it does not have a default value 112 if err != nil && defaultValue == "" { 113 missingVariables = append(missingVariables, name) 114 continue 115 } 116 } 117 118 if len(missingVariables) > 0 { 119 return rawArtifact, &errMissingVariables{missingVariables} 120 } 121 122 tmp, err = envsubst.Eval(tmp, func(in string) string { 123 v, _ := variablesClient(in) 124 return v 125 }) 126 if err != nil { 127 return rawArtifact, err 128 } 129 130 return []byte(tmp), err 131 } 132 133 type errMissingVariables struct { 134 Missing []string 135 } 136 137 func (e *errMissingVariables) Error() string { 138 sort.Strings(e.Missing) 139 return fmt.Sprintf( 140 "value for variables [%s] is not set. Please set the value using os environment variables or the clusterctl config file", 141 strings.Join(e.Missing, ", "), 142 ) 143 } 144 145 // inspectVariables parses through the yaml and returns a map of the variable 146 // names and if they have default values. It returns an error if it cannot 147 // parse the yaml. 148 func inspectVariables(data string) (map[string]string, error) { 149 variables := make(map[string]string) 150 t, err := parse.Parse(data) 151 if err != nil { 152 return nil, err 153 } 154 traverse(t.Root, variables) 155 return variables, nil 156 } 157 158 // traverse recursively walks down the root node and tracks the variables 159 // which are FuncNodes and if the variables have default values. 160 func traverse(root parse.Node, variables map[string]string) { 161 switch v := root.(type) { 162 case *parse.ListNode: 163 // iterate through the list node 164 for _, ln := range v.Nodes { 165 traverse(ln, variables) 166 } 167 case *parse.FuncNode: 168 if _, ok := variables[v.Param]; !ok { 169 // Build up a default value string 170 b := strings.Builder{} 171 for _, a := range v.Args { 172 switch w := a.(type) { 173 case *parse.FuncNode: 174 b.WriteString(fmt.Sprintf("${%s}", w.Param)) 175 case *parse.TextNode: 176 b.WriteString(w.Value) 177 } 178 } 179 // Key the variable name to its default string from the template, 180 // or to an empty string if it's required (no default). 181 variables[v.Param] = b.String() 182 } 183 } 184 } 185 186 // legacyVariableRegEx defines the regexp used for searching variables inside a YAML. 187 // It searches for variables with the format ${ VAR}, ${ VAR }, ${VAR }. 188 var legacyVariableRegEx = regexp.MustCompile(`(\${(\s+([A-Za-z0-9_$]+)\s+)})|(\${(\s+([A-Za-z0-9_$]+))})|(\${(([A-Za-z0-9_$]+)\s+)})`) 189 var whitespaceRegEx = regexp.MustCompile(`\s`) 190 191 // convertLegacyVars parses through the yaml string and modifies it replacing 192 // variables with the format ${ VAR}, ${ VAR }, ${VAR } to ${VAR}. This is 193 // done to maintain backwards compatibility. 194 func convertLegacyVars(data string) string { 195 return legacyVariableRegEx.ReplaceAllStringFunc(data, func(o string) string { 196 return whitespaceRegEx.ReplaceAllString(o, "") 197 }) 198 }