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