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  }