Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a porter configuration file #612

Merged
merged 2 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion cmd/porter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"os"

"github.com/deislabs/porter/pkg/config/datastore"
"github.com/deislabs/porter/pkg/porter"
"github.com/gobuffalo/packr/v2"
"github.com/spf13/cobra"
Expand All @@ -19,17 +20,26 @@ func main() {

func buildRootCommand() *cobra.Command {
p := porter.New()

cmd := &cobra.Command{
Use: "porter",
Short: "I am porter 👩🏽‍✈️, the friendly neighborhood CNAB authoring tool",
Example: ` porter create
porter build
porter install
porter uninstall`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
p.Config.DataLoader = datastore.FromFlagsThenEnvVarsThenConfigFile(cmd)
err := p.LoadData()
if err != nil {
return err
}

// Enable swapping out stdout/stderr for testing
p.Out = cmd.OutOrStdout()
p.Err = cmd.OutOrStderr()

return nil
},
SilenceUsage: true,
}
Expand Down
10 changes: 8 additions & 2 deletions docs/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,15 @@ defaultContentLanguage = "en"
name = "References"
identifier = "references"
weight = 600
[[menu.main]]
name = "Configuration"
url = "/configuration"
identifier = "configuration"
weight = 601
parent = "references"
[[menu.main]]
name = "CNAB Spec"
url = "https://cnab.io"
url = "https://deislabs.io/cnab"
identifier = "cnab"
weight = 600
weight = 610
parent = "references"
56 changes: 56 additions & 0 deletions docs/content/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: Configuration
description: Controlling Porter with its config file, environment variables and flags
---

Porter's configuration system has a precedence order:

* Flags (highest)
* Environment Variables
* Config File (lowest)

You may set a default value for a configuration value in the config file,
override it in a shell session with an environment variable and then override
both in a particular command with a flag.

## Flags

### Debug

`--debug` is a flag that is understood not only by the porter client but also the
runtime and most mixins. They may use it to print additional information that
may be useful when you think you may have found a bug, when you want to know
what commands they are executing, or when you need really verbose output to send
to the developers.

### Output

`--output` controls the format of the output printed by porter. Each command
supports a different set of allowed outputs though usually there is some
combination of: `table`, `json` and `yaml`.

## Environment Variables

Flags have corresponding environment variables that you can use so that you
don't need to manually set the flag every time. The flag will default to the
value of the environment variable, when defined.

`--flag` has a corresponding environment variable of `PORTER_FLAG`

For example, you can set `PORTER_DEBUG=true` and then all subsequent porter
commands will act as though the `--debug` flag was passed.

## Config File

Common flags can be defaulted in the config file. The config file is located in
the PORTER_HOME directory (**~/.porter**), is named **config** and can be in any
of the following file types: JSON, TOML, YAML, HCL, envfile and Java Properties
files.

Below is an example configuration file in TOML

**~/.porter/config.toml**
```toml
debug = true
output = "json"
```

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,39 @@ const (
var getExecutable = os.Executable
var evalSymlinks = filepath.EvalSymlinks

type DataStoreLoaderFunc func(*Config) error

type Config struct {
*context.Context
Data *Data

Manifest *Manifest
ManifestPath string
DataLoader DataStoreLoaderFunc

porterHome string
}

// New Config initializes a default porter configuration.
func New() *Config {
return &Config{
Context: context.New(),
Context: context.New(),
DataLoader: NoopDataLoader,
}
}

// LoadData from the datastore in PORTER_HOME.
// This defaults to doing nothing unless DataLoader has been set.
func (c *Config) LoadData() error {
c.Data = nil

if c.DataLoader == nil {
c.DataLoader = NoopDataLoader
}

return c.DataLoader(c)
}

// GetHomeDir determines the absolute path to the porter home directory.
func (c *Config) GetHomeDir() (string, error) {
if c.porterHome != "" {
Expand Down
14 changes: 14 additions & 0 deletions pkg/config/datastore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package config

// Data is the data stored in PORTER_HOME/porter.toml|yaml|json
type Data struct {
// Only define fields here that you need to access from code
// Values are dynamically applied to flags and don't need to be defined
}

var _ DataStoreLoaderFunc = NoopDataLoader

// NoopDataLoader skips loading the datastore.
func NoopDataLoader(config *Config) error {
return nil
}
74 changes: 74 additions & 0 deletions pkg/config/datastore/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package datastore

import (
"fmt"

"github.com/deislabs/porter/pkg/config"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

// FromFlagsThenEnvVarsThenConfigFile loads data with the following precedence:
// * Flags (highest)
// * Environment variables where --flag is assumed to be PORTER_FLAG
// * Config file (lowest)
func FromFlagsThenEnvVarsThenConfigFile(cmd *cobra.Command) config.DataStoreLoaderFunc {
return buildDataLoader(func(v *viper.Viper) {
v.SetEnvPrefix("PORTER")
v.AutomaticEnv()

// Apply the configuration file value to the flag when the flag is not set
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if !f.Changed && v.IsSet(f.Name) {
val := v.Get(f.Name)
cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
}
})
})
}

// FromConfigFile loads data from the config file only.
func FromConfigFile(cfg *config.Config) error {
dataloader := buildDataLoader(nil)
return dataloader(cfg)
}

func buildDataLoader(viperCfg func(v *viper.Viper)) config.DataStoreLoaderFunc {
return func(cfg *config.Config) error {
home, _ := cfg.GetHomeDir()

v := viper.New()
v.SetFs(cfg.FileSystem)
v.AddConfigPath(home)
err := v.ReadInConfig()

if viperCfg != nil {
viperCfg(v)
}

var data config.Data
if err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
data = DefaultDataStore()
} else {
return errors.Wrapf(err, "error reading config file at %q", v.ConfigFileUsed())
}
} else {
err = v.Unmarshal(&data)
if err != nil {
return errors.Wrapf(err, "error unmarshaling config at %q", v.ConfigFileUsed())
}
}

cfg.Data = &data

return nil
}
}

// DefaultDataStore used when no config file is found.
func DefaultDataStore() config.Data {
return config.Data{}
}
Loading