github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/generators/git.go (about)

     1  package generators
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/jeremywohl/flatten"
    13  	log "github.com/sirupsen/logrus"
    14  	"sigs.k8s.io/yaml"
    15  
    16  	"github.com/argoproj/argo-cd/v2/applicationset/services"
    17  	"github.com/argoproj/argo-cd/v2/applicationset/utils"
    18  	argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    19  )
    20  
    21  var _ Generator = (*GitGenerator)(nil)
    22  
    23  type GitGenerator struct {
    24  	repos services.Repos
    25  }
    26  
    27  func NewGitGenerator(repos services.Repos) Generator {
    28  	g := &GitGenerator{
    29  		repos: repos,
    30  	}
    31  	return g
    32  }
    33  
    34  func (g *GitGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
    35  	return &appSetGenerator.Git.Template
    36  }
    37  
    38  func (g *GitGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
    39  
    40  	// Return a requeue default of 3 minutes, if no default is specified.
    41  
    42  	if appSetGenerator.Git.RequeueAfterSeconds != nil {
    43  		return time.Duration(*appSetGenerator.Git.RequeueAfterSeconds) * time.Second
    44  	}
    45  
    46  	return DefaultRequeueAfterSeconds
    47  }
    48  
    49  func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]interface{}, error) {
    50  
    51  	if appSetGenerator == nil {
    52  		return nil, EmptyAppSetGeneratorError
    53  	}
    54  
    55  	if appSetGenerator.Git == nil {
    56  		return nil, EmptyAppSetGeneratorError
    57  	}
    58  
    59  	noRevisionCache := appSet.RefreshRequired()
    60  
    61  	var err error
    62  	var res []map[string]interface{}
    63  	if len(appSetGenerator.Git.Directories) != 0 {
    64  		res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
    65  	} else if len(appSetGenerator.Git.Files) != 0 {
    66  		res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
    67  	} else {
    68  		return nil, EmptyAppSetGeneratorError
    69  	}
    70  	if err != nil {
    71  		return nil, fmt.Errorf("error generating params from git: %w", err)
    72  	}
    73  
    74  	return res, nil
    75  }
    76  
    77  func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) {
    78  
    79  	// Directories, not files
    80  	allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, noRevisionCache)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("error getting directories from repo: %w", err)
    83  	}
    84  
    85  	log.WithFields(log.Fields{
    86  		"allPaths":        allPaths,
    87  		"total":           len(allPaths),
    88  		"repoURL":         appSetGenerator.Git.RepoURL,
    89  		"revision":        appSetGenerator.Git.Revision,
    90  		"pathParamPrefix": appSetGenerator.Git.PathParamPrefix,
    91  	}).Info("applications result from the repo service")
    92  
    93  	requestedApps := g.filterApps(appSetGenerator.Git.Directories, allPaths)
    94  
    95  	res, err := g.generateParamsFromApps(requestedApps, appSetGenerator, useGoTemplate, goTemplateOptions)
    96  	if err != nil {
    97  		return nil, fmt.Errorf("error generating params from apps: %w", err)
    98  	}
    99  
   100  	return res, nil
   101  }
   102  
   103  func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) {
   104  
   105  	// Get all files that match the requested path string, removing duplicates
   106  	allFiles := make(map[string][]byte)
   107  	for _, requestedPath := range appSetGenerator.Git.Files {
   108  		files, err := g.repos.GetFiles(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path, noRevisionCache)
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  		for filePath, content := range files {
   113  			allFiles[filePath] = content
   114  		}
   115  	}
   116  
   117  	// Extract the unduplicated map into a list, and sort by path to ensure a deterministic
   118  	// processing order in the subsequent step
   119  	allPaths := []string{}
   120  	for path := range allFiles {
   121  		allPaths = append(allPaths, path)
   122  	}
   123  	sort.Strings(allPaths)
   124  
   125  	// Generate params from each path, and return
   126  	res := []map[string]interface{}{}
   127  	for _, path := range allPaths {
   128  
   129  		// A JSON / YAML file path can contain multiple sets of parameters (ie it is an array)
   130  		paramsArray, err := g.generateParamsFromGitFile(path, allFiles[path], appSetGenerator.Git.Values, useGoTemplate, goTemplateOptions, appSetGenerator.Git.PathParamPrefix)
   131  		if err != nil {
   132  			return nil, fmt.Errorf("unable to process file '%s': %v", path, err)
   133  		}
   134  
   135  		res = append(res, paramsArray...)
   136  	}
   137  	return res, nil
   138  }
   139  
   140  func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []byte, values map[string]string, useGoTemplate bool, goTemplateOptions []string, pathParamPrefix string) ([]map[string]interface{}, error) {
   141  	objectsFound := []map[string]interface{}{}
   142  
   143  	// First, we attempt to parse as an array
   144  	err := yaml.Unmarshal(fileContent, &objectsFound)
   145  	if err != nil {
   146  		// If unable to parse as an array, attempt to parse as a single object
   147  		singleObj := make(map[string]interface{})
   148  		err = yaml.Unmarshal(fileContent, &singleObj)
   149  		if err != nil {
   150  			return nil, fmt.Errorf("unable to parse file: %v", err)
   151  		}
   152  		objectsFound = append(objectsFound, singleObj)
   153  	} else if len(objectsFound) == 0 {
   154  		// If file is valid but empty, add a default empty item
   155  		objectsFound = append(objectsFound, map[string]interface{}{})
   156  	}
   157  
   158  	res := []map[string]interface{}{}
   159  
   160  	for _, objectFound := range objectsFound {
   161  
   162  		params := map[string]interface{}{}
   163  
   164  		if useGoTemplate {
   165  			for k, v := range objectFound {
   166  				params[k] = v
   167  			}
   168  
   169  			paramPath := map[string]interface{}{}
   170  
   171  			paramPath["path"] = path.Dir(filePath)
   172  			paramPath["basename"] = path.Base(paramPath["path"].(string))
   173  			paramPath["filename"] = path.Base(filePath)
   174  			paramPath["basenameNormalized"] = utils.SanitizeName(path.Base(paramPath["path"].(string)))
   175  			paramPath["filenameNormalized"] = utils.SanitizeName(path.Base(paramPath["filename"].(string)))
   176  			paramPath["segments"] = strings.Split(paramPath["path"].(string), "/")
   177  			if pathParamPrefix != "" {
   178  				params[pathParamPrefix] = map[string]interface{}{"path": paramPath}
   179  			} else {
   180  				params["path"] = paramPath
   181  			}
   182  		} else {
   183  			flat, err := flatten.Flatten(objectFound, "", flatten.DotStyle)
   184  			if err != nil {
   185  				return nil, fmt.Errorf("error flattening object: %w", err)
   186  			}
   187  			for k, v := range flat {
   188  				params[k] = fmt.Sprintf("%v", v)
   189  			}
   190  			pathParamName := "path"
   191  			if pathParamPrefix != "" {
   192  				pathParamName = pathParamPrefix + "." + pathParamName
   193  			}
   194  			params[pathParamName] = path.Dir(filePath)
   195  			params[pathParamName+".basename"] = path.Base(params[pathParamName].(string))
   196  			params[pathParamName+".filename"] = path.Base(filePath)
   197  			params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName].(string)))
   198  			params[pathParamName+".filenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName+".filename"].(string)))
   199  			for k, v := range strings.Split(params[pathParamName].(string), "/") {
   200  				if len(v) > 0 {
   201  					params[pathParamName+"["+strconv.Itoa(k)+"]"] = v
   202  				}
   203  			}
   204  		}
   205  
   206  		err := appendTemplatedValues(values, params, useGoTemplate, goTemplateOptions)
   207  		if err != nil {
   208  			return nil, fmt.Errorf("failed to append templated values: %w", err)
   209  		}
   210  
   211  		res = append(res, params)
   212  	}
   213  
   214  	return res, nil
   215  }
   216  
   217  func (g *GitGenerator) filterApps(Directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allPaths []string) []string {
   218  	res := []string{}
   219  	for _, appPath := range allPaths {
   220  		appInclude := false
   221  		appExclude := false
   222  		// Iterating over each appPath and check whether directories object has requestedPath that matches the appPath
   223  		for _, requestedPath := range Directories {
   224  			match, err := path.Match(requestedPath.Path, appPath)
   225  			if err != nil {
   226  				log.WithError(err).WithField("requestedPath", requestedPath).
   227  					WithField("appPath", appPath).Error("error while matching appPath to requestedPath")
   228  				continue
   229  			}
   230  			if match && !requestedPath.Exclude {
   231  				appInclude = true
   232  			}
   233  			if match && requestedPath.Exclude {
   234  				appExclude = true
   235  			}
   236  		}
   237  		// Whenever there is a path with exclude: true it wont be included, even if it is included in a different path pattern
   238  		if appInclude && !appExclude {
   239  			res = append(res, appPath)
   240  		}
   241  	}
   242  	return res
   243  }
   244  
   245  func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) {
   246  	res := make([]map[string]interface{}, len(requestedApps))
   247  	for i, a := range requestedApps {
   248  
   249  		params := make(map[string]interface{}, 5)
   250  
   251  		if useGoTemplate {
   252  			paramPath := map[string]interface{}{}
   253  			paramPath["path"] = a
   254  			paramPath["basename"] = path.Base(a)
   255  			paramPath["basenameNormalized"] = utils.SanitizeName(path.Base(a))
   256  			paramPath["segments"] = strings.Split(paramPath["path"].(string), "/")
   257  			if appSetGenerator.Git.PathParamPrefix != "" {
   258  				params[appSetGenerator.Git.PathParamPrefix] = map[string]interface{}{"path": paramPath}
   259  			} else {
   260  				params["path"] = paramPath
   261  			}
   262  		} else {
   263  			pathParamName := "path"
   264  			if appSetGenerator.Git.PathParamPrefix != "" {
   265  				pathParamName = appSetGenerator.Git.PathParamPrefix + "." + pathParamName
   266  			}
   267  			params[pathParamName] = a
   268  			params[pathParamName+".basename"] = path.Base(a)
   269  			params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(a))
   270  			for k, v := range strings.Split(params[pathParamName].(string), "/") {
   271  				if len(v) > 0 {
   272  					params[pathParamName+"["+strconv.Itoa(k)+"]"] = v
   273  				}
   274  			}
   275  		}
   276  
   277  		err := appendTemplatedValues(appSetGenerator.Git.Values, params, useGoTemplate, goTemplateOptions)
   278  		if err != nil {
   279  			return nil, fmt.Errorf("failed to append templated values: %w", err)
   280  		}
   281  
   282  		res[i] = params
   283  	}
   284  
   285  	return res, nil
   286  }