github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/utils/utils.go (about) 1 package utils 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "crypto/x509" 7 "encoding/json" 8 "fmt" 9 "io" 10 "os" 11 "reflect" 12 "regexp" 13 "sort" 14 "strings" 15 "text/template" 16 "unsafe" 17 18 "github.com/Masterminds/sprig/v3" 19 "github.com/gosimple/slug" 20 "github.com/valyala/fasttemplate" 21 "sigs.k8s.io/yaml" 22 23 log "github.com/sirupsen/logrus" 24 25 argoappsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 26 ) 27 28 var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance 29 30 func init() { 31 // Avoid allowing the user to learn things about the environment. 32 delete(sprigFuncMap, "env") 33 delete(sprigFuncMap, "expandenv") 34 delete(sprigFuncMap, "getHostByName") 35 sprigFuncMap["normalize"] = SanitizeName 36 sprigFuncMap["slugify"] = SlugifyName 37 sprigFuncMap["toYaml"] = toYAML 38 sprigFuncMap["fromYaml"] = fromYAML 39 sprigFuncMap["fromYamlArray"] = fromYAMLArray 40 } 41 42 type Renderer interface { 43 RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.Application, error) 44 Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) 45 } 46 47 type Render struct { 48 } 49 50 func copyValueIntoUnexported(destination, value reflect.Value) { 51 reflect.NewAt(destination.Type(), unsafe.Pointer(destination.UnsafeAddr())). 52 Elem(). 53 Set(value) 54 } 55 56 func copyUnexported(copy, original reflect.Value) { 57 var unexported = reflect.NewAt(original.Type(), unsafe.Pointer(original.UnsafeAddr())).Elem() 58 copyValueIntoUnexported(copy, unexported) 59 } 60 61 func IsJSONStr(str string) bool { 62 str = strings.TrimSpace(str) 63 return len(str) > 0 && str[0] == '{' 64 } 65 66 func ConvertYAMLToJSON(str string) (string, error) { 67 if !IsJSONStr(str) { 68 jsonStr, err := yaml.YAMLToJSON([]byte(str)) 69 if err != nil { 70 return str, err 71 } 72 return string(jsonStr), nil 73 } 74 return str, nil 75 } 76 77 // This function is in charge of searching all String fields of the object recursively and apply templating 78 // thanks to https://gist.github.com/randallmlough/1fd78ec8a1034916ca52281e3b886dc7 79 func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) error { 80 switch original.Kind() { 81 // The first cases handle nested structures and translate them recursively 82 // If it is a pointer we need to unwrap and call once again 83 case reflect.Ptr: 84 // To get the actual value of the original we have to call Elem() 85 // At the same time this unwraps the pointer so we don't end up in 86 // an infinite recursion 87 originalValue := original.Elem() 88 // Check if the pointer is nil 89 if !originalValue.IsValid() { 90 return nil 91 } 92 // Allocate a new object and set the pointer to it 93 if originalValue.CanSet() { 94 copy.Set(reflect.New(originalValue.Type())) 95 } else { 96 copyUnexported(copy, original) 97 } 98 // Unwrap the newly created pointer 99 if err := r.deeplyReplace(copy.Elem(), originalValue, replaceMap, useGoTemplate, goTemplateOptions); err != nil { 100 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 101 return err 102 } 103 104 // If it is an interface (which is very similar to a pointer), do basically the 105 // same as for the pointer. Though a pointer is not the same as an interface so 106 // note that we have to call Elem() after creating a new object because otherwise 107 // we would end up with an actual pointer 108 case reflect.Interface: 109 // Get rid of the wrapping interface 110 originalValue := original.Elem() 111 // Create a new object. Now new gives us a pointer, but we want the value it 112 // points to, so we have to call Elem() to unwrap it 113 114 if originalValue.IsValid() { 115 reflectType := originalValue.Type() 116 117 reflectValue := reflect.New(reflectType) 118 119 copyValue := reflectValue.Elem() 120 if err := r.deeplyReplace(copyValue, originalValue, replaceMap, useGoTemplate, goTemplateOptions); err != nil { 121 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 122 return err 123 } 124 copy.Set(copyValue) 125 } 126 127 // If it is a struct we translate each field 128 case reflect.Struct: 129 for i := 0; i < original.NumField(); i += 1 { 130 var currentType = fmt.Sprintf("%s.%s", original.Type().Field(i).Name, original.Type().PkgPath()) 131 // specific case time 132 if currentType == "time.Time" { 133 copy.Field(i).Set(original.Field(i)) 134 } else if currentType == "Raw.k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" || currentType == "Raw.k8s.io/apimachinery/pkg/runtime" { 135 var unmarshaled interface{} 136 originalBytes := original.Field(i).Bytes() 137 convertedToJson, err := ConvertYAMLToJSON(string(originalBytes)) 138 if err != nil { 139 return fmt.Errorf("error while converting template to json %q: %w", convertedToJson, err) 140 } 141 err = json.Unmarshal([]byte(convertedToJson), &unmarshaled) 142 if err != nil { 143 return fmt.Errorf("failed to unmarshal JSON field: %w", err) 144 } 145 jsonOriginal := reflect.ValueOf(&unmarshaled) 146 jsonCopy := reflect.New(jsonOriginal.Type()).Elem() 147 err = r.deeplyReplace(jsonCopy, jsonOriginal, replaceMap, useGoTemplate, goTemplateOptions) 148 if err != nil { 149 return fmt.Errorf("failed to deeply replace JSON field contents: %w", err) 150 } 151 jsonCopyInterface := jsonCopy.Interface().(*interface{}) 152 data, err := json.Marshal(jsonCopyInterface) 153 if err != nil { 154 return fmt.Errorf("failed to marshal templated JSON field: %w", err) 155 } 156 copy.Field(i).Set(reflect.ValueOf(data)) 157 } else if err := r.deeplyReplace(copy.Field(i), original.Field(i), replaceMap, useGoTemplate, goTemplateOptions); err != nil { 158 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 159 return err 160 } 161 } 162 163 // If it is a slice we create a new slice and translate each element 164 case reflect.Slice: 165 if copy.CanSet() { 166 copy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap())) 167 } else { 168 copyValueIntoUnexported(copy, reflect.MakeSlice(original.Type(), original.Len(), original.Cap())) 169 } 170 171 for i := 0; i < original.Len(); i += 1 { 172 if err := r.deeplyReplace(copy.Index(i), original.Index(i), replaceMap, useGoTemplate, goTemplateOptions); err != nil { 173 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 174 return err 175 } 176 } 177 178 // If it is a map we create a new map and translate each value 179 case reflect.Map: 180 if copy.CanSet() { 181 copy.Set(reflect.MakeMap(original.Type())) 182 } else { 183 copyValueIntoUnexported(copy, reflect.MakeMap(original.Type())) 184 } 185 for _, key := range original.MapKeys() { 186 originalValue := original.MapIndex(key) 187 if originalValue.Kind() != reflect.String && isNillable(originalValue) && originalValue.IsNil() { 188 continue 189 } 190 // New gives us a pointer, but again we want the value 191 copyValue := reflect.New(originalValue.Type()).Elem() 192 193 if err := r.deeplyReplace(copyValue, originalValue, replaceMap, useGoTemplate, goTemplateOptions); err != nil { 194 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 195 return err 196 } 197 198 // Keys can be templated as well as values (e.g. to template something into an annotation). 199 if key.Kind() == reflect.String { 200 templatedKey, err := r.Replace(key.String(), replaceMap, useGoTemplate, goTemplateOptions) 201 if err != nil { 202 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 203 return err 204 } 205 key = reflect.ValueOf(templatedKey) 206 } 207 208 copy.SetMapIndex(key, copyValue) 209 } 210 211 // Otherwise we cannot traverse anywhere so this finishes the recursion 212 // If it is a string translate it (yay finally we're doing what we came for) 213 case reflect.String: 214 strToTemplate := original.String() 215 templated, err := r.Replace(strToTemplate, replaceMap, useGoTemplate, goTemplateOptions) 216 if err != nil { 217 // Not wrapping the error, since this is a recursive function. Avoids excessively long error messages. 218 return err 219 } 220 if copy.CanSet() { 221 copy.SetString(templated) 222 } else { 223 copyValueIntoUnexported(copy, reflect.ValueOf(templated)) 224 } 225 return nil 226 227 // And everything else will simply be taken from the original 228 default: 229 if copy.CanSet() { 230 copy.Set(original) 231 } else { 232 copyUnexported(copy, original) 233 } 234 } 235 return nil 236 } 237 238 // isNillable returns true if the value is something which may be set to nil. This function is meant to guard against a 239 // panic from calling IsNil on a non-pointer type. 240 func isNillable(v reflect.Value) bool { 241 switch v.Kind() { 242 case reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice: 243 return true 244 } 245 return false 246 } 247 248 func (r *Render) RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.Application, error) { 249 if tmpl == nil { 250 return nil, fmt.Errorf("application template is empty") 251 } 252 253 if len(params) == 0 { 254 return tmpl, nil 255 } 256 257 original := reflect.ValueOf(tmpl) 258 copy := reflect.New(original.Type()).Elem() 259 260 if err := r.deeplyReplace(copy, original, params, useGoTemplate, goTemplateOptions); err != nil { 261 return nil, err 262 } 263 264 replacedTmpl := copy.Interface().(*argoappsv1.Application) 265 266 // Add the 'resources-finalizer' finalizer if: 267 // The template application doesn't have any finalizers, and: 268 // a) there is no syncPolicy, or 269 // b) there IS a syncPolicy, but preserveResourcesOnDeletion is set to false 270 // See TestRenderTemplateParamsFinalizers in util_test.go for test-based definition of behaviour 271 if (syncPolicy == nil || !syncPolicy.PreserveResourcesOnDeletion) && 272 ((*replacedTmpl).ObjectMeta.Finalizers == nil || len((*replacedTmpl).ObjectMeta.Finalizers) == 0) { 273 274 (*replacedTmpl).ObjectMeta.Finalizers = []string{"resources-finalizer.argocd.argoproj.io"} 275 } 276 277 return replacedTmpl, nil 278 } 279 280 func (r *Render) RenderGeneratorParams(gen *argoappsv1.ApplicationSetGenerator, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.ApplicationSetGenerator, error) { 281 if gen == nil { 282 return nil, fmt.Errorf("generator is empty") 283 } 284 285 if len(params) == 0 { 286 return gen, nil 287 } 288 289 original := reflect.ValueOf(gen) 290 copy := reflect.New(original.Type()).Elem() 291 292 if err := r.deeplyReplace(copy, original, params, useGoTemplate, goTemplateOptions); err != nil { 293 return nil, fmt.Errorf("failed to replace parameters in generator: %w", err) 294 } 295 296 replacedGen := copy.Interface().(*argoappsv1.ApplicationSetGenerator) 297 298 return replacedGen, nil 299 } 300 301 var isTemplatedRegex = regexp.MustCompile(".*{{.*}}.*") 302 303 // Replace executes basic string substitution of a template with replacement values. 304 // remaining in the substituted template. 305 func (r *Render) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { 306 if useGoTemplate { 307 template, err := template.New("").Funcs(sprigFuncMap).Parse(tmpl) 308 if err != nil { 309 return "", fmt.Errorf("failed to parse template %s: %w", tmpl, err) 310 } 311 for _, option := range goTemplateOptions { 312 template = template.Option(option) 313 } 314 315 var replacedTmplBuffer bytes.Buffer 316 if err = template.Execute(&replacedTmplBuffer, replaceMap); err != nil { 317 return "", fmt.Errorf("failed to execute go template %s: %w", tmpl, err) 318 } 319 320 return replacedTmplBuffer.String(), nil 321 } 322 323 if !isTemplatedRegex.MatchString(tmpl) { 324 return tmpl, nil 325 } 326 327 fstTmpl, err := fasttemplate.NewTemplate(tmpl, "{{", "}}") 328 if err != nil { 329 return "", fmt.Errorf("invalid template: %w", err) 330 } 331 replacedTmpl := fstTmpl.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { 332 trimmedTag := strings.TrimSpace(tag) 333 replacement, ok := replaceMap[trimmedTag].(string) 334 if len(trimmedTag) == 0 || !ok { 335 return w.Write([]byte(fmt.Sprintf("{{%s}}", tag))) 336 } 337 return w.Write([]byte(replacement)) 338 }) 339 return replacedTmpl, nil 340 } 341 342 // Log a warning if there are unrecognized generators 343 func CheckInvalidGenerators(applicationSetInfo *argoappsv1.ApplicationSet) error { 344 hasInvalidGenerators, invalidGenerators := invalidGenerators(applicationSetInfo) 345 var errorMessage error 346 if len(invalidGenerators) > 0 { 347 gnames := []string{} 348 for n := range invalidGenerators { 349 gnames = append(gnames, n) 350 } 351 sort.Strings(gnames) 352 aname := applicationSetInfo.ObjectMeta.Name 353 msg := "ApplicationSet %s contains unrecognized generators: %s" 354 errorMessage = fmt.Errorf(msg, aname, strings.Join(gnames, ", ")) 355 log.Warnf(msg, aname, strings.Join(gnames, ", ")) 356 } else if hasInvalidGenerators { 357 name := applicationSetInfo.ObjectMeta.Name 358 msg := "ApplicationSet %s contains unrecognized generators" 359 errorMessage = fmt.Errorf(msg, name) 360 log.Warnf(msg, name) 361 } 362 return errorMessage 363 } 364 365 // Return true if there are unknown generators specified in the application set. If we can discover the names 366 // of these generators, return the names as the keys in a map 367 func invalidGenerators(applicationSetInfo *argoappsv1.ApplicationSet) (bool, map[string]bool) { 368 names := make(map[string]bool) 369 hasInvalidGenerators := false 370 for index, generator := range applicationSetInfo.Spec.Generators { 371 v := reflect.Indirect(reflect.ValueOf(generator)) 372 found := false 373 for i := 0; i < v.NumField(); i++ { 374 field := v.Field(i) 375 if !field.CanInterface() { 376 continue 377 } 378 if !reflect.ValueOf(field.Interface()).IsNil() { 379 found = true 380 break 381 } 382 } 383 if !found { 384 hasInvalidGenerators = true 385 addInvalidGeneratorNames(names, applicationSetInfo, index) 386 } 387 } 388 return hasInvalidGenerators, names 389 } 390 391 func addInvalidGeneratorNames(names map[string]bool, applicationSetInfo *argoappsv1.ApplicationSet, index int) { 392 // The generator names are stored in the "kubectl.kubernetes.io/last-applied-configuration" annotation 393 config := applicationSetInfo.ObjectMeta.Annotations["kubectl.kubernetes.io/last-applied-configuration"] 394 var values map[string]interface{} 395 err := json.Unmarshal([]byte(config), &values) 396 if err != nil { 397 log.Warnf("couldn't unmarshal kubectl.kubernetes.io/last-applied-configuration: %+v", config) 398 return 399 } 400 401 spec, ok := values["spec"].(map[string]interface{}) 402 if !ok { 403 log.Warn("coundn't get spec from kubectl.kubernetes.io/last-applied-configuration annotation") 404 return 405 } 406 407 generators, ok := spec["generators"].([]interface{}) 408 if !ok { 409 log.Warn("coundn't get generators from kubectl.kubernetes.io/last-applied-configuration annotation") 410 return 411 } 412 413 if index >= len(generators) { 414 log.Warnf("index %d out of range %d for generator in kubectl.kubernetes.io/last-applied-configuration", index, len(generators)) 415 return 416 } 417 418 generator, ok := generators[index].(map[string]interface{}) 419 if !ok { 420 log.Warn("coundn't get generator from kubectl.kubernetes.io/last-applied-configuration annotation") 421 return 422 } 423 424 for key := range generator { 425 names[key] = true 426 break 427 } 428 } 429 430 func NormalizeBitbucketBasePath(basePath string) string { 431 if strings.HasSuffix(basePath, "/rest/") { 432 return strings.TrimSuffix(basePath, "/") 433 } 434 if !strings.HasSuffix(basePath, "/rest") { 435 return basePath + "/rest" 436 } 437 return basePath 438 } 439 440 // SlugifyName generates a URL-friendly slug from the provided name and additional options. 441 // The slug is generated in accordance with the following rules: 442 // 1. The generated slug will be URL-safe and suitable for use in URLs. 443 // 2. The maximum length of the slug can be specified using the `maxSize` argument. 444 // 3. Smart truncation can be enabled or disabled using the `EnableSmartTruncate` argument. 445 // 4. The input name can be any string value that needs to be converted into a slug. 446 // 447 // Args: 448 // - args: A variadic number of arguments where: 449 // - The first argument (if provided) is an integer specifying the maximum length of the slug. 450 // - The second argument (if provided) is a boolean indicating whether smart truncation is enabled. 451 // - The last argument (if provided) is the input name that needs to be slugified. 452 // If no name is provided, an empty string will be used. 453 // 454 // Returns: 455 // - string: The generated URL-friendly slug based on the input name and options. 456 func SlugifyName(args ...interface{}) string { 457 // Default values for arguments 458 maxSize := 50 459 EnableSmartTruncate := true 460 name := "" 461 462 // Process the arguments 463 for idx, arg := range args { 464 switch idx { 465 case len(args) - 1: 466 name = arg.(string) 467 case 0: 468 maxSize = arg.(int) 469 case 1: 470 EnableSmartTruncate = arg.(bool) 471 default: 472 log.Errorf("Bad 'slugify' arguments.") 473 } 474 } 475 476 sanitizedName := SanitizeName(name) 477 478 // Configure slug generation options 479 slug.EnableSmartTruncate = EnableSmartTruncate 480 slug.MaxLength = maxSize 481 482 // Generate the slug from the input name 483 urlSlug := slug.Make(sanitizedName) 484 485 return urlSlug 486 } 487 488 func getTlsConfigWithCACert(scmRootCAPath string) *tls.Config { 489 490 tlsConfig := &tls.Config{} 491 492 if scmRootCAPath != "" { 493 _, err := os.Stat(scmRootCAPath) 494 if os.IsNotExist(err) { 495 log.Errorf("scmRootCAPath '%s' specified does not exist: %s", scmRootCAPath, err) 496 return tlsConfig 497 } 498 rootCA, err := os.ReadFile(scmRootCAPath) 499 if err != nil { 500 log.Errorf("error reading certificate from file '%s', proceeding without custom rootCA : %s", scmRootCAPath, err) 501 return tlsConfig 502 } 503 certPool := x509.NewCertPool() 504 ok := certPool.AppendCertsFromPEM([]byte(rootCA)) 505 if !ok { 506 log.Errorf("failed to append certificates from PEM: proceeding without custom rootCA") 507 } else { 508 tlsConfig.RootCAs = certPool 509 } 510 } 511 return tlsConfig 512 } 513 514 func GetTlsConfig(scmRootCAPath string, insecure bool) *tls.Config { 515 tlsConfig := getTlsConfigWithCACert(scmRootCAPath) 516 517 if insecure { 518 tlsConfig.InsecureSkipVerify = true 519 } 520 return tlsConfig 521 }