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  }