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 }