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  }