Skip to content

Commit

Permalink
envgen v1
Browse files Browse the repository at this point in the history
  • Loading branch information
s1moe2 committed Apr 20, 2020
0 parents commit ab59589
Show file tree
Hide file tree
Showing 8 changed files with 533 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.DS_Store
.idea
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# envgen

_envgen_ is CLI tool that generates .env files for subpackages in your project based on a configuration file.

The purpose of this tool is to:
- avoid using a bash script: harder to maintain, test, lacks the safety
of a statically typed and compiled language like Go;
- provide flexibility and ease of use/reuse: additional features can be
easily added and made available to projects already using the tool.

### Usage

`$ envgen config.yaml`

Here's the structure of the configuration file:

```yaml
branchVarName: CIRCLE_BRANCH
branchVarDefault: develop

branches:
- name: develop
suffix: _DEV
- name: staging
suffix: _STG

packages:
- package: awesome-crawler
variables:
- AC_THRESHOLD
- AC_TITLE
- package: web-server
variables:
- WS_PORT
- WS_ADDRESS

globals:
- V_DATABASE
```
Configuration details:
| Key | Description |
| ----------------- |:-------------:|
| branchVarName | name of the env var that contains the CI branch (CIRCLE_BRANCH), for CircleCI |
| branchVarDefault | default value of `branchVarName` |
| branches | list of branches and their suffixes |
| packages | list of subpackages |
| packages.package | subpackage path to where the .env file will be written |
| packages.variables| subpackage environment variables to generate |
| globals | list of variables that are environment independent and global to all subpackages |

For each entry in the `packages` array, a `.env` file will be created in the path `package`,
with the variables defined in `variables` and `globals`.

#### Example:

Project structure:
```
├── bla.go
├── awesome-crawler
│ └── index.js
└── web-server
└── index.html
```

Loaded env vars:
```
CIRCLE_BRANCH=develop
V_DATABASE=somedburl
WS_ADDRESS_DEV=www.sample.web
WS_PORT_DEV=1234
AC_TITLE_DEV=sometitle
AC_THRESHOLD_DEV=50
```

Resulting structure:
```
├── bla.go
├── awesome-crawler
│ ├── .env
│ └── index.js
└── web-server
├── .env
└── index.html
```

Content of awesome-crawler/.env:
```
V_DATABASE=somedburl
AC_TITLE=sometitle
AC_THRESHOLD=50
```

Content of web-server/.env:
```
V_DATABASE=somedburl
WS_ADDRESS=www.sample.web
WS_PORT=1234
```
174 changes: 174 additions & 0 deletions cmd/envgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"io/ioutil"
"os"

"gopkg.in/yaml.v2"
)

type ConfBranches struct {
Name string `yaml:"name"`
Suffix string `yaml:"suffix"`
}

type ConfPackages struct {
Package string `yaml:"package"`
Variables []string `yaml:"variables"`
}

type GeneratorConfig struct {
BranchVarName string `yaml:"branchVarName"`
BranchVarDefault string `yaml:"branchVarDefault"`
Branches []ConfBranches `yaml:"branches"`
Packages []ConfPackages `yaml:"packages"`
Globals []string `yaml:"globals"`
}

type Generator struct {
conf GeneratorConfig
branchSuffix string
}

var rootCmd = &cobra.Command{
Version: "1.0.0",
SilenceErrors: true,
Use: "envgen <configFilePath>",
Short: "envgen generates env files for sub packages",
Long: "envgen is CLI tool that generates .env files for subpackages in your project based on a configuration file",
Args: cobra.ExactArgs(1),
RunE: generateEnvFiles,
}

func Execute() {
rootCmd.SetErr(errorWriter{})
if err := rootCmd.Execute(); err != nil {
rootCmd.PrintErr(err)
os.Exit(1)
}
}

func generateEnvFiles(cmd *cobra.Command, args []string) error {
gen := &Generator{}
err := gen.loadConfig(args[0])
if err != nil {
return err
}

logInfo("Starting env files generation")
fmt.Println()

globals, err := getVariablesValues(gen.conf.Globals, "")
if err != nil {
return err
}

for _, g := range gen.conf.Packages {
pkg := g.Package
logInfo("> Loading variables for " + pkg)

// generate package specific vars
packageVars, err := getVariablesValues(g.Variables, gen.branchSuffix)
if err != nil {
return err
}

// append globals
packageVars = append(packageVars, globals...)

logInfo("> Writing env file for " + pkg)
err = writeFile(fmt.Sprintf("%s/.env", pkg), packageVars)
if err != nil {
return err
}

logInfo("> Done generating env file for " + pkg)
fmt.Println()
}

logInfo("Finished env files generation!")
return nil
}

func getVariablesValues(envVars []string, suffix string) ([]string, error) {
vars := []string{}
for _, v := range envVars {
val, ok := os.LookupEnv(v + suffix)
if !ok {
err := fmt.Errorf("missing variable %s", v)
return nil, err
}

vars = append(vars, fmt.Sprintf("%s=%s", v, val))
}

return vars, nil
}

// loadConfig loads the configuration from the provided yaml file
// into the instance of the Generator. It also determines the branch suffix property.
func (g *Generator) loadConfig(filepath string) error {
config := &GeneratorConfig{}
file, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}

err = yaml.Unmarshal(file, config)
if err != nil {
return err
}

g.conf = *config

g.branchSuffix, err = g.findBranchSuffix()
if err != nil {
return fmt.Errorf("missing branch suffix")
}

return nil
}

// findBranchSuffix determines the branch suffix to use, depending on the current CI branch
func (g *Generator) findBranchSuffix() (string, error) {
branch := getEnv(g.conf.BranchVarName, "", g.conf.BranchVarDefault)

for _, b := range g.conf.Branches {
if b.Name == branch {
return b.Suffix, nil
}
}

return "", fmt.Errorf("could not find branch suffix")
}

// getEnv looks up for a loaded environment variable.
// An optional suffix may be passed, as well as a default value to return if the env var is not loaded.
func getEnv(key string, suffix string, defaultVal string) string {
if value, exists := os.LookupEnv(key + suffix); exists {
return value
}

return defaultVal
}

// writeFile writes a slice of strings into a file, separated by new lines
func writeFile(path string, vars []string) error {
file, err := os.Create(path)
if err != nil {
return err
}

defer file.Close()

sep := "\n"
for _, line := range vars {
if _, err = file.WriteString(line + sep); err != nil {
return err
}
}

return nil
}
24 changes: 24 additions & 0 deletions cmd/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cmd

import (
"fmt"
"os"
)

const (
InfoColor = "\033[1;34m%s\033[0m\n"
ErrorColor = "\033[1;31m%s\033[0m\n"
)

func printLog(color string, msg interface{}) {
fmt.Printf(color, msg)
}

func logInfo(msg interface{}) {
printLog(InfoColor, msg)
}

type errorWriter struct {}
func (w errorWriter) Write(p []byte) (n int, err error) {
return fmt.Fprintf(os.Stdout, ErrorColor, p)
}
19 changes: 19 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module envgen

go 1.14

require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/mitchellh/mapstructure v1.2.2 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.0.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.3 // indirect
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
golang.org/x/text v0.3.2 // indirect
gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/yaml.v2 v2.2.8
)
Loading

0 comments on commit ab59589

Please sign in to comment.