github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/tanka/find.go (about)

     1  package tanka
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"runtime"
     7  	"time"
     8  
     9  	"github.com/grafana/tanka/pkg/jsonnet"
    10  	"github.com/grafana/tanka/pkg/jsonnet/jpath"
    11  	"github.com/grafana/tanka/pkg/spec/v1alpha1"
    12  	"github.com/pkg/errors"
    13  	"github.com/rs/zerolog/log"
    14  	"k8s.io/apimachinery/pkg/labels"
    15  )
    16  
    17  // FindOpts are optional arguments for FindEnvs
    18  type FindOpts struct {
    19  	JsonnetOpts
    20  	Selector    labels.Selector
    21  	Parallelism int
    22  }
    23  
    24  // FindEnvs returns metadata of all environments recursively found in 'path'.
    25  // Each directory is tested and included if it is a valid environment, either
    26  // static or inline. If a directory is a valid environment, its subdirectories
    27  // are not checked.
    28  func FindEnvs(path string, opts FindOpts) ([]*v1alpha1.Environment, error) {
    29  	return findEnvsFromPaths([]string{path}, opts)
    30  }
    31  
    32  // FindEnvsFromPaths does the same as FindEnvs but takes a list of paths instead
    33  func FindEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) {
    34  	return findEnvsFromPaths(paths, opts)
    35  }
    36  
    37  func findEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) {
    38  	if opts.Parallelism <= 0 {
    39  		opts.Parallelism = runtime.NumCPU()
    40  	}
    41  
    42  	log.Debug().Int("parallelism", opts.Parallelism).Int("paths", len(paths)).Msg("Finding Tanka environments")
    43  	startTime := time.Now()
    44  
    45  	jsonnetFiles, err := findJsonnetFilesFromPaths(paths, opts)
    46  	if err != nil {
    47  		return nil, fmt.Errorf("finding jsonnet files: %w", err)
    48  	}
    49  
    50  	findJsonnetFilesEndTime := time.Now()
    51  
    52  	envs, err := findEnvsFromJsonnetFiles(jsonnetFiles, opts)
    53  	if err != nil {
    54  		return nil, fmt.Errorf("finding environments: %w", err)
    55  	}
    56  
    57  	findEnvsEndTime := time.Now()
    58  
    59  	log.Info().
    60  		Int("environments", len(envs)).
    61  		Dur("ms_to_find_jsonnet_files", findJsonnetFilesEndTime.Sub(startTime)).
    62  		Dur("ms_to_find_environments", findEnvsEndTime.Sub(findJsonnetFilesEndTime)).
    63  		Msg("Found Tanka environments")
    64  
    65  	return envs, nil
    66  }
    67  
    68  // find all jsonnet files within given paths
    69  func findJsonnetFilesFromPaths(paths []string, opts FindOpts) ([]string, error) {
    70  	type findJsonnetFilesOut struct {
    71  		jsonnetFiles []string
    72  		err          error
    73  	}
    74  
    75  	pathChan := make(chan string, len(paths))
    76  	findJsonnetFilesChan := make(chan findJsonnetFilesOut)
    77  	for i := 0; i < opts.Parallelism; i++ {
    78  		go func() {
    79  			for path := range pathChan {
    80  				jsonnetFiles, err := jsonnet.FindFiles(path, nil)
    81  				var mainFiles []string
    82  				for _, file := range jsonnetFiles {
    83  					if filepath.Base(file) == jpath.DefaultEntrypoint {
    84  						mainFiles = append(mainFiles, file)
    85  					}
    86  				}
    87  				findJsonnetFilesChan <- findJsonnetFilesOut{jsonnetFiles: mainFiles, err: err}
    88  			}
    89  		}()
    90  	}
    91  
    92  	// push paths to channel
    93  	for _, path := range paths {
    94  		pathChan <- path
    95  	}
    96  	close(pathChan)
    97  
    98  	// collect jsonnet files
    99  	var jsonnetFiles []string
   100  	var errs []error
   101  	for range paths {
   102  		res := <-findJsonnetFilesChan
   103  		if res.err != nil {
   104  			errs = append(errs, res.err)
   105  			continue
   106  		}
   107  		jsonnetFiles = append(jsonnetFiles, res.jsonnetFiles...)
   108  	}
   109  	close(findJsonnetFilesChan)
   110  
   111  	if len(errs) != 0 {
   112  		return jsonnetFiles, ErrParallel{errors: errs}
   113  	}
   114  
   115  	return jsonnetFiles, nil
   116  }
   117  
   118  // find all environments within jsonnet files
   119  func findEnvsFromJsonnetFiles(jsonnetFiles []string, opts FindOpts) ([]*v1alpha1.Environment, error) {
   120  	type findEnvsOut struct {
   121  		envs []*v1alpha1.Environment
   122  		err  error
   123  	}
   124  
   125  	jsonnetFilesChan := make(chan string, len(jsonnetFiles))
   126  	findEnvsChan := make(chan findEnvsOut)
   127  
   128  	for i := 0; i < opts.Parallelism; i++ {
   129  		go func() {
   130  			for jsonnetFile := range jsonnetFilesChan {
   131  				// try if this has envs
   132  				list, err := List(jsonnetFile, Opts{JsonnetOpts: opts.JsonnetOpts})
   133  				if err != nil &&
   134  					// expected when looking for environments
   135  					!errors.As(err, &jpath.ErrorNoBase{}) &&
   136  					!errors.As(err, &jpath.ErrorFileNotFound{}) {
   137  					findEnvsChan <- findEnvsOut{err: fmt.Errorf("%s:\n %w", jsonnetFile, err)}
   138  					continue
   139  				}
   140  				filtered := []*v1alpha1.Environment{}
   141  				// optionally filter
   142  				if opts.Selector != nil && !opts.Selector.Empty() {
   143  					for _, e := range list {
   144  						if !opts.Selector.Matches(e.Metadata) {
   145  							continue
   146  						}
   147  						filtered = append(filtered, e)
   148  					}
   149  				} else {
   150  					filtered = append(filtered, list...)
   151  				}
   152  				findEnvsChan <- findEnvsOut{envs: filtered, err: nil}
   153  			}
   154  		}()
   155  	}
   156  
   157  	// push jsonnet files to channel
   158  	for _, jsonnetFile := range jsonnetFiles {
   159  		jsonnetFilesChan <- jsonnetFile
   160  	}
   161  	close(jsonnetFilesChan)
   162  
   163  	// collect environments
   164  	var envs []*v1alpha1.Environment
   165  	var errs []error
   166  	for i := 0; i < len(jsonnetFiles); i++ {
   167  		res := <-findEnvsChan
   168  		if res.err != nil {
   169  			errs = append(errs, res.err)
   170  			continue
   171  		}
   172  		envs = append(envs, res.envs...)
   173  	}
   174  	close(findEnvsChan)
   175  
   176  	if len(errs) != 0 {
   177  		return envs, ErrParallel{errors: errs}
   178  	}
   179  
   180  	return envs, nil
   181  }