get.porter.sh/porter@v1.3.0/pkg/config/config.go (about) 1 package config 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "reflect" 10 "strings" 11 12 "get.porter.sh/porter/pkg/experimental" 13 "get.porter.sh/porter/pkg/portercontext" 14 "get.porter.sh/porter/pkg/schema" 15 "get.porter.sh/porter/pkg/tracing" 16 "github.com/spf13/viper" 17 "go.opentelemetry.io/otel/attribute" 18 ) 19 20 const ( 21 // Name is the file name of the porter configuration file. 22 Name = "porter.yaml" 23 24 // EnvHOME is the name of the environment variable containing the porter home directory path. 25 EnvHOME = "PORTER_HOME" 26 27 // EnvBundleName is the name of the environment variable containing the name of the bundle. 28 EnvBundleName = "CNAB_BUNDLE_NAME" 29 30 // EnvInstallationName is the name of the environment variable containing the name of the installation. 31 EnvInstallationName = "CNAB_INSTALLATION_NAME" 32 33 // EnvACTION is the requested action to be executed 34 EnvACTION = "CNAB_ACTION" 35 36 // EnvDEBUG is a custom porter parameter that signals that --debug flag has been passed through from the client to the runtime. 37 EnvDEBUG = "PORTER_DEBUG" 38 39 // CustomPorterKey is the key in the bundle.json custom section that contains the Porter stamp 40 // It holds all the metadata that Porter includes that is specific to Porter about the bundle. 41 CustomPorterKey = "sh.porter" 42 43 // BundleOutputsDir is the directory where outputs are expected to be placed 44 // during the execution of a bundle action. 45 BundleOutputsDir = "/cnab/app/outputs" 46 47 // ClaimFilepath is the filepath to the claim.json inside of an bundle image 48 ClaimFilepath = "/cnab/claim.json" 49 50 // EnvPorterInstallationNamespace is the name of the environment variable which is injected into the 51 // bundle image, containing the namespace of the installation. 52 EnvPorterInstallationNamespace = "PORTER_INSTALLATION_NAMESPACE" 53 54 // EnvPorterInstallationName is the name of the environment variable which is injected into the 55 // bundle image, containing the name of the installation. 56 EnvPorterInstallationName = "PORTER_INSTALLATION_NAME" 57 58 // DefaultVerbosity is the default value for the --verbosity flag. 59 DefaultVerbosity = "info" 60 ) 61 62 // These are functions that afero doesn't support, so this lets us stub them out for tests to set the 63 // location of the current executable porter binary and resolve PORTER_HOME. 64 var getExecutable = os.Executable 65 var evalSymlinks = filepath.EvalSymlinks 66 67 // DataStoreLoaderFunc defines the Config.DataLoader function signature 68 // used to load data into Config.DataStore. 69 type DataStoreLoaderFunc func(context.Context, *Config, map[string]interface{}) error 70 71 type Config struct { 72 *portercontext.Context 73 Data Data 74 DataLoader DataStoreLoaderFunc 75 76 // ConfigFilePath is the path to the loaded configuration file 77 ConfigFilePath string 78 79 // Cache the resolved Porter home directory 80 porterHome string 81 82 // Cache the resolved Porter binary path 83 porterPath string 84 85 // parsed feature flags 86 experimental *experimental.FeatureFlags 87 88 // list of variables used in the config file 89 // for example: secret.NAME, or env.NAME 90 templateVariables []string 91 92 // the populated viper instance that loaded the current configuration 93 viper *viper.Viper 94 } 95 96 // New Config initializes a default porter configuration. 97 func New() *Config { 98 return NewFor(portercontext.New()) 99 } 100 101 // NewFor initializes a porter configuration, using an existing porter context. 102 func NewFor(pCtx *portercontext.Context) *Config { 103 return &Config{ 104 Context: pCtx, 105 Data: DefaultDataStore(), 106 DataLoader: LoadFromEnvironment(), 107 } 108 } 109 110 func (c *Config) NewLogConfiguration() portercontext.LogConfiguration { 111 return portercontext.LogConfiguration{ 112 Verbosity: c.GetVerbosity().Level(), 113 StructuredLogs: c.Data.Logs.Structured, 114 LogToFile: c.Data.Logs.LogToFile, 115 LogDirectory: filepath.Join(c.porterHome, "logs"), 116 LogLevel: c.Data.Logs.Level.Level(), 117 TelemetryEnabled: c.Data.Telemetry.Enabled, 118 TelemetryEndpoint: c.Data.Telemetry.Endpoint, 119 TelemetryProtocol: c.Data.Telemetry.Protocol, 120 TelemetryInsecure: c.Data.Telemetry.Insecure, 121 TelemetryCertificate: c.Data.Telemetry.Certificate, 122 TelemetryCompression: c.Data.Telemetry.Compression, 123 TelemetryTimeout: c.Data.Telemetry.Timeout, 124 TelemetryHeaders: c.Data.Telemetry.Headers, 125 TelemetryServiceName: "porter", 126 TelemetryDirectory: filepath.Join(c.porterHome, "traces"), 127 TelemetryRedirectToFile: c.Data.Telemetry.RedirectToFile, 128 TelemetryStartTimeout: c.Data.Telemetry.GetStartTimeout(), 129 } 130 } 131 132 // loadData from the datastore defined in PORTER_HOME, and render the 133 // config file using the specified template data. 134 func (c *Config) loadData(ctx context.Context, templateData map[string]interface{}) (context.Context, error) { 135 if c.DataLoader == nil { 136 c.DataLoader = LoadFromEnvironment() 137 } 138 139 if err := c.DataLoader(ctx, c, templateData); err != nil { 140 return ctx, err 141 } 142 143 // Now that we have completely loaded our config, configure our final logging/tracing 144 ctx = c.Context.ConfigureLogging(ctx, c.NewLogConfiguration()) 145 return ctx, nil 146 } 147 148 func (c *Config) GetSchemaCheckStrategy(ctx context.Context) schema.CheckStrategy { 149 switch c.Data.SchemaCheck { 150 case string(schema.CheckStrategyMinor): 151 return schema.CheckStrategyMinor 152 case string(schema.CheckStrategyMajor): 153 return schema.CheckStrategyMajor 154 case string(schema.CheckStrategyNone): 155 return schema.CheckStrategyNone 156 case string(schema.CheckStrategyExact), "": 157 return schema.CheckStrategyExact 158 default: 159 log := tracing.LoggerFromContext(ctx) 160 log.Warnf("invalid schema-check value specified %q, defaulting to exact", c.Data.SchemaCheck) 161 return schema.CheckStrategyExact 162 } 163 } 164 165 func (c *Config) GetStorage(name string) (StoragePlugin, error) { 166 if c != nil { 167 for _, is := range c.Data.StoragePlugins { 168 if is.Name == name { 169 return is, nil 170 } 171 } 172 } 173 174 return StoragePlugin{}, fmt.Errorf("store '%s' not defined", name) 175 } 176 177 func (c *Config) GetSecretsPlugin(name string) (SecretsPlugin, error) { 178 if c != nil { 179 for _, cs := range c.Data.SecretsPlugin { 180 if cs.Name == name { 181 return cs, nil 182 } 183 } 184 } 185 186 return SecretsPlugin{}, errors.New("secrets %q not defined") 187 } 188 189 func (c *Config) GetSigningPlugin(name string) (SigningPlugin, error) { 190 if c != nil { 191 for _, cs := range c.Data.SigningPlugin { 192 if cs.Name == name { 193 return cs, nil 194 } 195 } 196 } 197 198 return SigningPlugin{}, errors.New("signing %q not defined") 199 } 200 201 // GetHomeDir determines the absolute path to the porter home directory. 202 // Hierarchy of checks: 203 // - PORTER_HOME 204 // - HOME/.porter or USERPROFILE/.porter 205 func (c *Config) GetHomeDir() (string, error) { 206 if c.porterHome != "" { 207 return c.porterHome, nil 208 } 209 210 home := c.Getenv(EnvHOME) 211 if home == "" { 212 userHome, err := os.UserHomeDir() 213 if err != nil { 214 return "", fmt.Errorf("could not get user home directory: %w", err) 215 } 216 home = filepath.Join(userHome, ".porter") 217 } 218 219 // As a relative path may be supplied via EnvHOME, 220 // we want to return the absolute path for programmatic usage elsewhere, 221 // for instance, in setting up volume mounts for outputs 222 c.SetHomeDir(c.FileSystem.Abs(home)) 223 224 return c.porterHome, nil 225 } 226 227 // SetHomeDir is a test function that allows tests to use an alternate 228 // Porter home directory. 229 func (c *Config) SetHomeDir(home string) { 230 c.porterHome = home 231 232 // Set this as an environment variable so that when we spawn new processes 233 // such as a mixin or plugin, that they can find PORTER_HOME too 234 c.Setenv(EnvHOME, home) 235 } 236 237 // SetPorterPath is a test function that allows tests to use an alternate 238 // Porter binary location. 239 func (c *Config) SetPorterPath(path string) { 240 c.porterPath = path 241 } 242 243 func (c *Config) GetPorterPath(ctx context.Context) (string, error) { 244 if c.porterPath != "" { 245 return c.porterPath, nil 246 } 247 248 log := tracing.LoggerFromContext(ctx) 249 porterPath, err := getExecutable() 250 if err != nil { 251 return "", log.Error(fmt.Errorf("could not get path to the executing porter binary: %w", err)) 252 } 253 254 // We try to resolve back to the original location 255 hardPath, err := evalSymlinks(porterPath) 256 if err != nil { // if we have trouble resolving symlinks, skip trying to help people who used symlinks 257 return "", log.Error(fmt.Errorf("WARNING could not resolve %s for symbolic links: %w", porterPath, err)) 258 } 259 if hardPath != porterPath { 260 log.Debugf("Resolved porter binary from %s to %s", porterPath, hardPath) 261 porterPath = hardPath 262 } 263 264 c.porterPath = porterPath 265 return porterPath, nil 266 } 267 268 // GetBundlesCache locates the bundle cache from the porter home directory. 269 func (c *Config) GetBundlesCache() (string, error) { 270 home, err := c.GetHomeDir() 271 if err != nil { 272 return "", err 273 } 274 return filepath.Join(home, "bundles"), nil 275 } 276 277 func (c *Config) GetPluginsDir() (string, error) { 278 home, err := c.GetHomeDir() 279 if err != nil { 280 return "", err 281 } 282 return filepath.Join(home, "plugins"), nil 283 } 284 285 func (c *Config) GetPluginPath(plugin string) (string, error) { 286 pluginsDir, err := c.GetPluginsDir() 287 if err != nil { 288 return "", err 289 } 290 291 executablePath := filepath.Join(pluginsDir, plugin, plugin) 292 return executablePath, nil 293 } 294 295 // GetBundleArchiveLogs locates the output for Bundle Archive Operations. 296 func (c *Config) GetBundleArchiveLogs() (string, error) { 297 home, err := c.GetHomeDir() 298 if err != nil { 299 return "", err 300 } 301 return filepath.Join(home, "archives"), nil 302 } 303 304 // GetFeatureFlags indicates which experimental feature flags are enabled 305 func (c *Config) GetFeatureFlags() experimental.FeatureFlags { 306 if c.experimental == nil { 307 flags := experimental.ParseFlags(c.Data.ExperimentalFlags) 308 c.experimental = &flags 309 } 310 return *c.experimental 311 } 312 313 // IsFeatureEnabled returns true if the specified experimental flag is enabled. 314 func (c *Config) IsFeatureEnabled(flag experimental.FeatureFlags) bool { 315 return c.GetFeatureFlags()&flag == flag 316 } 317 318 // SetExperimentalFlags programmatically, overriding Config.Data.ExperimentalFlags. 319 // Example: Config.SetExperimentalFlags(experimental.FlagStructuredLogs | ...) 320 func (c *Config) SetExperimentalFlags(flags experimental.FeatureFlags) { 321 c.experimental = &flags 322 } 323 324 // GetBuildDriver determines the correct build driver to use, taking 325 // into account experimental flags. 326 // Use this instead of Config.Data.BuildDriver directly. 327 func (c *Config) GetBuildDriver() string { 328 return BuildDriverBuildkit 329 } 330 331 // GetVerbosity converts the user-specified verbosity flag into a LogLevel enum. 332 func (c *Config) GetVerbosity() LogLevel { 333 return ParseLogLevel(c.Data.Verbosity) 334 } 335 336 // Load loads the configuration file, rendering any templating used in the config file 337 // such as ${secret.NAME} or ${env.NAME}. 338 // Pass nil for resolveSecret to skip resolving secrets. 339 func (c *Config) Load(ctx context.Context, resolveSecret func(ctx context.Context, secretKey string) (string, error)) (context.Context, error) { 340 ctx, log := tracing.StartSpan(ctx) 341 defer log.EndSpan() 342 343 ctx, err := c.loadFirstPass(ctx) 344 if err != nil { 345 return ctx, err 346 } 347 348 ctx, err = c.loadFinalPass(ctx, resolveSecret) 349 if err != nil { 350 return ctx, err 351 } 352 353 // Record some global configuration values that are relevant to most commands 354 log.SetAttributes( 355 attribute.String("porter.config.namespace", c.Data.Namespace), 356 attribute.String("porter.config.experimental", strings.Join(c.Data.ExperimentalFlags, ",")), 357 ) 358 359 return ctx, nil 360 } 361 362 // our first pass only loads the config file while replacing 363 // environment variables. Once we have that we can use the 364 // config to connect to a secret store and do a second pass 365 // over the config. 366 func (c *Config) loadFirstPass(ctx context.Context) (context.Context, error) { 367 ctx, log := tracing.StartSpan(ctx) 368 defer log.EndSpan() 369 370 templateData := map[string]interface{}{ 371 "env": c.EnvironMap(), 372 } 373 return c.loadData(ctx, templateData) 374 } 375 376 func (c *Config) loadFinalPass(ctx context.Context, resolveSecret func(ctx context.Context, secretKey string) (string, error)) (context.Context, error) { 377 ctx, log := tracing.StartSpan(ctx) 378 defer log.EndSpan() 379 380 // Don't do extra work if there aren't any secrets 381 if len(c.templateVariables) == 0 || resolveSecret == nil { 382 return ctx, nil 383 } 384 385 secrets := make(map[string]string, len(c.templateVariables)) 386 for _, variable := range c.templateVariables { 387 err := func(variable string) error { 388 // Check if it's a secret variable, e.g. ${secret.NAME} 389 secretPrefix := "secret." 390 i := strings.Index(variable, secretPrefix) 391 if i == -1 { 392 return nil 393 } 394 395 secretKey := variable[len(secretPrefix):] 396 397 ctx, childLog := log.StartSpanWithName("resolveSecret", attribute.String("porter.config.secret.key", secretKey)) 398 defer childLog.EndSpan() 399 secretValue, err := resolveSecret(ctx, secretKey) 400 if err != nil { 401 return childLog.Error(fmt.Errorf("could not render config file because ${secret.%s} could not be resolved: %w", secretKey, err)) 402 } 403 404 secrets[secretKey] = secretValue 405 return nil 406 }(variable) 407 if err != nil { 408 return ctx, err 409 } 410 } 411 412 templateData := map[string]interface{}{ 413 "env": c.EnvironMap(), 414 "secret": secrets, 415 } 416 417 // reload configuration with secrets loaded 418 return c.loadData(ctx, templateData) 419 } 420 421 // ExportRemoteConfigAsEnvironmentVariables represents the current configuration 422 // as environment variables suitable for a remote Porter actor, such as a mixin 423 // or plugin. Only a subset of values are exported, such as tracing and logging, 424 // and not plugin configuration (since it's not relevant when running a plugin 425 // and may contain sensitive data). For example, if Config.Data.Logs is set to warn, it 426 // would return PORTER_LOGS_LEVEL=warn in the resulting set of environment variables. 427 // This is used to pass config from porter to a mixin or plugin. 428 func (c *Config) ExportRemoteConfigAsEnvironmentVariables() []string { 429 if c.viper == nil { 430 return nil 431 } 432 433 // the set of config that is relevant to remote actors 434 keepPrefixes := []string{"verbosity", "logs", "telemetry"} 435 436 var env []string 437 for _, key := range c.viper.AllKeys() { 438 for _, prefix := range keepPrefixes { 439 if strings.HasPrefix(key, prefix) { 440 val := c.viper.Get(key) 441 if reflect.ValueOf(val).IsZero() { 442 continue 443 } 444 envVarSuffix := strings.ToUpper(key) 445 envVarSuffix = strings.NewReplacer(".", "_", "-", "_"). 446 Replace(envVarSuffix) 447 envVar := fmt.Sprintf("PORTER_%s", envVarSuffix) 448 env = append(env, fmt.Sprintf("%s=%v", envVar, val)) 449 } 450 } 451 } 452 453 return env 454 }