get.porter.sh/porter@v1.3.0/pkg/config/loader.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"sort"
     9  	"strings"
    10  
    11  	"get.porter.sh/porter/pkg/tracing"
    12  	"github.com/jeremywohl/flatten"
    13  	"github.com/mitchellh/mapstructure"
    14  	"github.com/osteele/liquid"
    15  	"github.com/osteele/liquid/render"
    16  	"github.com/spf13/viper"
    17  	"go.opentelemetry.io/otel/attribute"
    18  )
    19  
    20  var _ DataStoreLoaderFunc = NoopDataLoader
    21  
    22  // NoopDataLoader skips loading the datastore.
    23  func NoopDataLoader(_ context.Context, _ *Config, _ map[string]interface{}) error {
    24  	return nil
    25  }
    26  
    27  // LoadFromEnvironment loads data with the following precedence:
    28  // * Environment variables where --flag is assumed to be PORTER_FLAG
    29  // * Config file
    30  // * Flag default (lowest)
    31  func LoadFromEnvironment() DataStoreLoaderFunc {
    32  	return LoadFromViper(BindViperToEnvironmentVariables, nil)
    33  }
    34  
    35  func BindViperToEnvironmentVariables(v *viper.Viper) {
    36  	v.SetEnvPrefix("PORTER")
    37  	v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
    38  	v.AutomaticEnv()
    39  
    40  	// Bind open telemetry environment variables
    41  	// See https://github.com/open-telemetry/opentelemetry-go/tree/main/exporters/otlp/otlptrace
    42  	var err error
    43  	if err = v.BindEnv("telemetry.endpoint", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); err != nil {
    44  		_ = errors.Unwrap(err)
    45  	}
    46  	if err = v.BindEnv("telemetry.protocol", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"); err != nil {
    47  		_ = errors.Unwrap(err)
    48  	}
    49  	if err = v.BindEnv("telemetry.insecure", "OTEL_EXPORTER_OTLP_INSECURE", "OTEL_EXPORTER_OTLP_TRACES_INSECURE"); err != nil {
    50  		_ = errors.Unwrap(err)
    51  	}
    52  	if err = v.BindEnv("telemetry.certificate", "OTEL_EXPORTER_OTLP_CERTIFICATE", "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"); err != nil {
    53  		_ = errors.Unwrap(err)
    54  	}
    55  	if err = v.BindEnv("telemetry.headers", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_EXPORTER_OTLP_TRACES_HEADERS"); err != nil {
    56  		_ = errors.Unwrap(err)
    57  	}
    58  	if err = v.BindEnv("telemetry.compression", "OTEL_EXPORTER_OTLP_COMPRESSION", "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION"); err != nil {
    59  		_ = errors.Unwrap(err)
    60  	}
    61  	if err = v.BindEnv("telemetry.timeout", "OTEL_EXPORTER_OTLP_TIMEOUT", "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"); err != nil {
    62  		_ = errors.Unwrap(err)
    63  	}
    64  }
    65  
    66  // LoadFromFilesystem loads data with the following precedence:
    67  // * Config file
    68  // * Flag default (lowest)
    69  // This is used for testing only.
    70  func LoadFromFilesystem() DataStoreLoaderFunc {
    71  	return LoadFromViper(nil, nil)
    72  }
    73  
    74  // LoadFromViper loads data from a configurable viper instance.
    75  func LoadFromViper(viperCfg func(v *viper.Viper), cobraCfg func(v *viper.Viper)) DataStoreLoaderFunc {
    76  	return func(ctx context.Context, cfg *Config, templateData map[string]interface{}) error {
    77  		home, _ := cfg.GetHomeDir()
    78  
    79  		_, log := tracing.StartSpanWithName(ctx, "LoadFromViper", attribute.String("porter.PORTER_HOME", home))
    80  		defer log.EndSpan()
    81  
    82  		v := viper.New()
    83  		v.SetFs(cfg.FileSystem)
    84  
    85  		// Consider an empty environment variable as "set", so that you can do things like
    86  		// PORTER_DEFAULT_STORAGE="" and have that override what's in the config file.
    87  		v.AllowEmptyEnv(true)
    88  
    89  		// Initialize empty config
    90  		// 2024-12-23: This is still needed, otherwise TestLegacyPluginAdapter fails.
    91  		err := setDefaultsFrom(v, cfg.Data)
    92  		if err != nil {
    93  			return log.Error(fmt.Errorf("error initializing configuration data: %w", err))
    94  		}
    95  
    96  		if viperCfg != nil {
    97  			viperCfg(v)
    98  		}
    99  
   100  		// Find the config file
   101  		v.AddConfigPath(home)
   102  
   103  		// Only read the config file if we are running as porter
   104  		// Skip it for internal plugins since we pass the resolved
   105  		// config directly to the plugins
   106  		if !cfg.IsInternalPlugin {
   107  			err = v.ReadInConfig()
   108  			if err != nil {
   109  				if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
   110  					return log.Error(fmt.Errorf("error reading config file: %w", err))
   111  				}
   112  			}
   113  		}
   114  
   115  		cfgFile := v.ConfigFileUsed()
   116  		if cfgFile != "" {
   117  			log.SetAttributes(attribute.String("porter.PORTER_CONFIG", cfgFile))
   118  
   119  			cfgContents, err := cfg.FileSystem.ReadFile(cfgFile)
   120  			if err != nil {
   121  				return log.Error(fmt.Errorf("error reading config file template: %w", err))
   122  			}
   123  
   124  			// Render any template variables used in the config file
   125  			engine := liquid.NewEngine()
   126  			engine.Delims("${", "}", "${%", "%}")
   127  			tmpl, err := engine.ParseTemplate(cfgContents)
   128  			if err != nil {
   129  				return log.Error(fmt.Errorf("error parsing config file as a liquid template:\n%s\n\n: %w", cfgContents, err))
   130  			}
   131  
   132  			finalCfg, err := tmpl.Render(templateData)
   133  			if err != nil {
   134  				return log.Error(fmt.Errorf("error rendering config file as a liquid template:\n%s\n\n: %w", cfgContents, err))
   135  			}
   136  
   137  			// Remember what variables are used in the template
   138  			// we use this to resolve variables in the second pass over the config file
   139  			if len(cfg.templateVariables) == 0 {
   140  				cfg.templateVariables = listTemplateVariables(tmpl)
   141  			}
   142  
   143  			if err := v.ReadConfig(bytes.NewReader(finalCfg)); err != nil {
   144  				return log.Error(fmt.Errorf("error loading configuration file: %w", err))
   145  			}
   146  		}
   147  
   148  		// Porter can be used through the CLI, in which case give it a chance to hook up cobra command flags to viper
   149  		if cobraCfg != nil {
   150  			cobraCfg(v)
   151  		}
   152  
   153  		// Bind viper back to the configuration data only after all viper and cobra setup is completed
   154  		if err := v.Unmarshal(&cfg.Data); err != nil {
   155  			return fmt.Errorf("error unmarshaling viper config as porter config: %w", err)
   156  		}
   157  
   158  		cfg.viper = v
   159  		return nil
   160  	}
   161  }
   162  
   163  func setDefaultsFrom(v *viper.Viper, val interface{}) error {
   164  	var tmp map[string]interface{}
   165  	err := mapstructure.Decode(val, &tmp)
   166  	if err != nil {
   167  		return fmt.Errorf("error decoding configuration from struct: %v", err)
   168  	}
   169  
   170  	defaults, err := flatten.Flatten(tmp, "", flatten.DotStyle)
   171  	if err != nil {
   172  		return fmt.Errorf("error flattening default configuration from struct: %v", err)
   173  	}
   174  	for defaultKey, defaultValue := range defaults {
   175  		v.SetDefault(defaultKey, defaultValue)
   176  	}
   177  	return nil
   178  }
   179  
   180  func listTemplateVariables(tmpl *liquid.Template) []string {
   181  	vars := map[string]struct{}{}
   182  	findTemplateVariables(tmpl.GetRoot(), vars)
   183  
   184  	results := make([]string, 0, len(vars))
   185  	for v := range vars {
   186  		results = append(results, v)
   187  	}
   188  	sort.Strings(results)
   189  
   190  	return results
   191  }
   192  
   193  // findTemplateVariables looks at the template's abstract syntax tree (AST)
   194  // and identifies which variables were used
   195  func findTemplateVariables(curNode render.Node, vars map[string]struct{}) {
   196  	switch v := curNode.(type) {
   197  	case *render.SeqNode:
   198  		for _, childNode := range v.Children {
   199  			findTemplateVariables(childNode, vars)
   200  		}
   201  	case *render.ObjectNode:
   202  		vars[v.Args] = struct{}{}
   203  	}
   204  }