Skip to content
This repository has been archived by the owner on Dec 26, 2023. It is now read-only.

Commit

Permalink
Merge pull request #84 from ergomake/config-set-context
Browse files Browse the repository at this point in the history
introduce config set-context command
  • Loading branch information
vieiralucas authored Sep 12, 2023
2 parents 5812c1a + 836d701 commit 62405bb
Show file tree
Hide file tree
Showing 11 changed files with 360 additions and 128 deletions.
24 changes: 17 additions & 7 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,24 @@ jobs:

- run: go install .

- name: Create Layerform config file
- name: layerform config set-context
run: |
mkdir -p ~/.layerform
echo "currentContext: test" > ~/.layerform/config
echo "contexts:" >> ~/.layerform/config
echo " test:" >> ~/.layerform/config
echo " type: local" >> ~/.layerform/config
echo " dir: test" >> ~/.layerform/config
# validations, fails if command succeeds
! layerform config set-context test -t local # missing --dir
! layerform config set-context test -t s3 --bucket bucket # missing region
! layerform config set-context test -t s3 --region region # missing bucket
! layerform config set-context test -t cloud --url "invalid url" --email [email protected] --password strongpass
! layerform config set-context test -t cloud --url https://a.b.com --email invalid --password strongpass
! layerform config set-context test -t cloud --email invalid --password strongpass # missing url
! layerform config set-context test -t cloud --url https://a.b.com --password strongpass # missing email
! layerform config set-context test -t cloud --url https://a.b.com --email [email protected] # missing password
# set valid contexts
layerform config set-context test-s3 -t s3 --bucket bucket --region us-east-1
layerform config set-context test-cloud -t cloud --url https://demo.layerform.dev --email [email protected] --password strongpass
layerform config set-context test-local -t local --dir test
- name: Configure
run: |
Expand Down
17 changes: 17 additions & 0 deletions cmd/cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cli

import (
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(configCmd)
}

var configCmd = &cobra.Command{
Use: "config",
Short: "Modify layerform config file",
Long: `Modify layerform config file using subcomands like "layerform config set-context"`,
Example: `# Set a context entry in config
layerform config set-context example --type=local --dir=~/.layerform/contexts/example`,
}
118 changes: 118 additions & 0 deletions cmd/cli/set_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cli

import (
"fmt"
"os"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/ergomake/layerform/internal/lfconfig"
)

func init() {
configSetContextCmd.Flags().StringP("type", "t", "local", "type of the context entry, must be \"local\", \"s3\" or \"cloud\"")
configSetContextCmd.Flags().String("dir", "", "directory to store definitions and instances, required when type is \"local\"")
configSetContextCmd.Flags().String("bucket", "", "bucket to store definitions and instances, required when type is \"s3\"")
configSetContextCmd.Flags().String("region", "", "region where bucket is located, required when type is \"s3\"")
configSetContextCmd.Flags().String("url", "", "url of layerform cloud, required when type is \"cloud\"")
configSetContextCmd.Flags().String("email", "", "email of layerform cloud user, required when type is \"cloud\"")
configSetContextCmd.Flags().String("password", "", "password of layerform cloud user, required when type is \"cloud\"")
configSetContextCmd.Flags().SortFlags = false

configCmd.AddCommand(configSetContextCmd)
}

var configSetContextCmd = &cobra.Command{
Use: "set-context <name>",
Short: "Set a context entry in layerform config file",
Long: `Set a context entry in layerform config file.
Specifying a name that already exists will update that context values unless the type is different.`,
Example: `# Set a context of type local named local-example
layerform config set-context local-example -t local --dir example-dir
# Set a context of type s3 named s3-example
layerform config set-context s3-example -t s3 --bucket example-bucket --region us-east-1
# Set a context of type cloud named cloud-example
layerform config set-context cloud-example -t cloud --url https://example.layerform.dev --email [email protected] --password secretpass`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]

t, _ := cmd.Flags().GetString("type")
configCtx := lfconfig.ConfigContext{Type: t}
switch configCtx.Type {
case "local":
dir, _ := cmd.Flags().GetString("dir")
configCtx.Type = t
configCtx.Dir = strings.TrimSpace(dir)
case "s3":
bucket, _ := cmd.Flags().GetString("bucket")
region, _ := cmd.Flags().GetString("region")
configCtx.Bucket = strings.TrimSpace(bucket)
configCtx.Region = strings.TrimSpace(region)
case "cloud":
url, _ := cmd.Flags().GetString("url")
email, _ := cmd.Flags().GetString("email")
password, _ := cmd.Flags().GetString("password")
configCtx.URL = strings.TrimSpace(url)
configCtx.Email = strings.TrimSpace(email)
configCtx.Password = strings.TrimSpace(password)
default:
fmt.Fprintf(os.Stderr, "invalid type %s\n", configCtx.Type)
os.Exit(1)
}

err := lfconfig.Validate(configCtx)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "invalid context configuration"))
os.Exit(1)
}

cfg, err := lfconfig.Load("")
if err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "fail to open config file"))
os.Exit(1)
}

action := "modified"
if cfg == nil {
action = "created"
cfg, err = lfconfig.Init(name, configCtx, "")
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "fail to initialize empty config"))
os.Exit(1)
}
} else {
prev, ok := cfg.Contexts[name]
if !ok {
action = "created"
}

if ok && prev.Type != t {
fmt.Fprintf(
os.Stderr,
"%s context already exists with a different type of %s, context type can't be updated.\n",
name,
cfg.GetCurrent().Type,
)
os.Exit(1)
}
cfg.Contexts[name] = configCtx
}

cfg.CurrentContext = name

err = cfg.Save()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "fail to save config file"))
os.Exit(1)
}

fmt.Fprintf(os.Stdout, "Context \"%s\" %s.\n", name, action)
},
SilenceErrors: true,
}
129 changes: 69 additions & 60 deletions internal/lfconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ import (

type configFile struct {
CurrentContext string `yaml:"currentContext"`
Contexts map[string]configContext `yaml:"contexts"`
Contexts map[string]ConfigContext `yaml:"contexts"`
}

type configContext struct {
Type string `yaml:"type"`
Dir string `yaml:"dir,omitempty"`
Bucket string `yaml:"bucket,omitempty"`
Region string `yaml:"region,omitempty"`
type ConfigContext struct {
Type string `yaml:"type"`
Dir string `yaml:"dir,omitempty"`
Bucket string `yaml:"bucket,omitempty"`
Region string `yaml:"region,omitempty"`
URL string `yaml:"url,omitempty"`
Email string `yaml:"email,omitempty"`
Password string `yaml:"password,omitempty"`
}

func getDefaultPaths() ([]string, error) {
Expand All @@ -35,13 +38,13 @@ func getDefaultPaths() ([]string, error) {
}

return []string{
path.Join(homedir, ".layerform", "config"),
path.Join(homedir, ".layerform", "configurations.yaml"),
path.Join(homedir, ".layerform", "configurations.yml"),
path.Join(homedir, ".layerform", "configuration.yaml"),
path.Join(homedir, ".layerform", "configuration.yml"),
path.Join(homedir, ".layerform", "config.yaml"),
path.Join(homedir, ".layerform", "config.yml"),
path.Join(homedir, ".layerform", "config"),
}, nil
}

Expand All @@ -50,6 +53,27 @@ type config struct {
path string
}

func Init(name string, ctx ConfigContext, path string) (*config, error) {
ctxs := map[string]ConfigContext{}
ctxs[name] = ctx
cfgFile := &configFile{
CurrentContext: name,
Contexts: ctxs,
}
if path == "" {
paths, err := getDefaultPaths()
if err != nil {
return nil, err
}
path = paths[0]
}

return &config{
cfgFile,
path,
}, nil
}

func Load(path string) (*config, error) {
paths := []string{path}
if path == "" {
Expand Down Expand Up @@ -94,12 +118,27 @@ func Load(path string) (*config, error) {
return nil, err
}

func (c *config) getCurrent() configContext {
func (cfg *config) Save() error {
data, err := yaml.Marshal(cfg.configFile)
if err != nil {
return errors.Wrap(err, "fail to encode config file to yaml")
}

err = os.MkdirAll(path.Dir(cfg.path), 0755)
if err != nil {
return errors.Wrap(err, "fail to create config file dir")
}

err = os.WriteFile(cfg.path, data, 0644)
return errors.Wrap(err, "fail to write config file")
}

func (c *config) GetCurrent() ConfigContext {
return c.Contexts[c.CurrentContext]
}

func (c *config) getDir() string {
dir := c.getCurrent().Dir
dir := c.GetCurrent().Dir
if !path.IsAbs(dir) {
dir = path.Join(path.Dir(c.path), dir)
}
Expand All @@ -110,19 +149,13 @@ func (c *config) getDir() string {
const stateFileName = "layerform.lfstate"

func (c *config) GetInstancesBackend(ctx context.Context) (layerinstances.Backend, error) {
current := c.getCurrent()
current := c.GetCurrent()
var blob storage.FileLike
switch current.Type {
case "local":
blob = storage.NewFileStorage(path.Join(c.getDir(), stateFileName))
case "ergomake":
// TODO: hardcode production ergomake url here
baseURL := os.Getenv("LF_ERGOMAKE_URL")
if baseURL == "" {
return nil, errors.New("attempt to use ergomake backend but no LF_ERGOMAKE_URL in env")
}

return layerinstances.NewErgomake(baseURL), nil
case "cloud":
return layerinstances.NewCloud(current.URL), nil
case "s3":
b, err := storage.NewS3Backend(current.Bucket, stateFileName, current.Region)
if err != nil {
Expand All @@ -137,17 +170,11 @@ func (c *config) GetInstancesBackend(ctx context.Context) (layerinstances.Backen
const definitionsFileName = "layerform.definitions.json"

func (c *config) GetDefinitionsBackend(ctx context.Context) (layerdefinitions.Backend, error) {
current := c.getCurrent()
current := c.GetCurrent()
var blob storage.FileLike
switch current.Type {
case "ergomake":
// TODO: hardcode production ergomake url here
baseURL := os.Getenv("LF_ERGOMAKE_URL")
if baseURL == "" {
return nil, errors.New("attempt to use ergomake backend but no LF_ERGOMAKE_URL in env")
}

return layerdefinitions.NewErgomake(baseURL), nil
case "cloud":
return layerdefinitions.NewCloud(current.URL), nil
case "local":
blob = storage.NewFileStorage(path.Join(c.getDir(), definitionsFileName))
case "s3":
Expand All @@ -162,17 +189,11 @@ func (c *config) GetDefinitionsBackend(ctx context.Context) (layerdefinitions.Ba
}

func (c *config) GetSpawnCommand(ctx context.Context) (spawn.Spawn, error) {
t := c.getCurrent().Type

switch t {
case "ergomake":
// TODO: hardcode production ergomake url here
baseURL := os.Getenv("LF_ERGOMAKE_URL")
if baseURL == "" {
return nil, errors.New("attempt to use ergomake backend but no LF_ERGOMAKE_URL in env")
}
current := c.GetCurrent()

return spawn.NewErgomake(baseURL), nil
switch current.Type {
case "cloud":
return spawn.NewCloud(current.URL), nil
case "s3":
fallthrough
case "local":
Expand All @@ -189,21 +210,15 @@ func (c *config) GetSpawnCommand(ctx context.Context) (spawn.Spawn, error) {
return spawn.NewLocal(layersBackend, instancesBackend), nil
}

return nil, errors.Errorf("fail to get spawn command unexpected context type %s", t)
return nil, errors.Errorf("fail to get spawn command unexpected context type %s", current.Type)
}

func (c *config) GetKillCommand(ctx context.Context) (kill.Kill, error) {
t := c.getCurrent().Type

switch t {
case "ergomake":
// TODO: hardcode production ergomake url here
baseURL := os.Getenv("LF_ERGOMAKE_URL")
if baseURL == "" {
return nil, errors.New("attempt to use ergomake backend but no LF_ERGOMAKE_URL in env")
}
current := c.GetCurrent()

return kill.NewErgomake(baseURL), nil
switch current.Type {
case "cloud":
return kill.NewCloud(current.URL), nil
case "s3":
fallthrough
case "local":
Expand All @@ -220,21 +235,15 @@ func (c *config) GetKillCommand(ctx context.Context) (kill.Kill, error) {
return kill.NewLocal(layersBackend, instancesBackend), nil
}

return nil, errors.Errorf("fail to get kill command unexpected context type %s", t)
return nil, errors.Errorf("fail to get kill command unexpected context type %s", current.Type)
}

func (c *config) GetRefreshCommand(ctx context.Context) (refresh.Refresh, error) {
t := c.getCurrent().Type

switch t {
case "ergomake":
// TODO: hardcode production ergomake url here
baseURL := os.Getenv("LF_ERGOMAKE_URL")
if baseURL == "" {
return nil, errors.New("attempt to use ergomake backend but no LF_ERGOMAKE_URL in env")
}
current := c.GetCurrent()

return refresh.NewErgomake(baseURL), nil
switch current.Type {
case "cloud":
return refresh.NewCloud(current.URL), nil
case "s3":
fallthrough
case "local":
Expand All @@ -251,5 +260,5 @@ func (c *config) GetRefreshCommand(ctx context.Context) (refresh.Refresh, error)
return refresh.NewLocal(layersBackend, instancesBackend), nil
}

return nil, errors.Errorf("fail to get spawn command unexpected context type %s", t)
return nil, errors.Errorf("fail to get spawn command unexpected context type %s", current.Type)
}
Loading

0 comments on commit 62405bb

Please sign in to comment.