gitflow: multi env support

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
This commit is contained in:
Andrea Luzzardi 2021-05-18 19:15:17 -07:00
parent f0156f449f
commit 90abadf0de
14 changed files with 262 additions and 211 deletions

View File

@ -9,13 +9,37 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
func GetCurrentEnvironmentState(ctx context.Context) *state.State { func CurrentWorkspace(ctx context.Context) *state.Workspace {
lg := log.Ctx(ctx) lg := log.Ctx(ctx)
// If no environment name has been given, look for the current environment if workspacePath := viper.GetString("workspace"); workspacePath != "" {
environment := viper.GetString("environment") workspace, err := state.Open(ctx, workspacePath)
if environment == "" { if err != nil {
st, err := state.Current(ctx) lg.
Fatal().
Err(err).
Str("path", workspacePath).
Msg("failed to open workspace")
}
return workspace
}
workspace, err := state.Current(ctx)
if err != nil {
lg.
Fatal().
Err(err).
Msg("failed to determine current workspace")
}
return workspace
}
func CurrentEnvironmentState(ctx context.Context, workspace *state.Workspace) *state.State {
lg := log.Ctx(ctx)
environmentName := viper.GetString("environment")
if environmentName != "" {
st, err := workspace.Get(ctx, environmentName)
if err != nil { if err != nil {
lg. lg.
Fatal(). Fatal().
@ -25,38 +49,33 @@ func GetCurrentEnvironmentState(ctx context.Context) *state.State {
return st return st
} }
// At this point, it must be an environment name environments, err := workspace.List(ctx)
workspace := viper.GetString("workspace")
var err error
if workspace == "" {
workspace, err = state.CurrentWorkspace(ctx)
if err != nil {
lg.
Fatal().
Err(err).
Msg("failed to determine current workspace")
}
}
environments, err := state.List(ctx, workspace)
if err != nil { if err != nil {
lg. lg.
Fatal(). Fatal().
Err(err). Err(err).
Msg("failed to list environments") Msg("failed to list environments")
} }
for _, e := range environments {
if e.Name == environment { if len(environments) == 0 {
return e lg.
} Fatal().
Msg("no environments")
} }
lg. if len(environments) > 1 {
Fatal(). envNames := []string{}
Str("environment", environment). for _, e := range environments {
Msg("environment not found") envNames = append(envNames, e.Name)
}
lg.
Fatal().
Err(err).
Strs("environments", envNames).
Msg("multiple environments available in the workspace, select one with `--environment`")
}
return nil return environments[0]
} }
// Re-compute an environment (equivalent to `dagger up`). // Re-compute an environment (equivalent to `dagger up`).

View File

@ -2,7 +2,6 @@ package cmd
import ( import (
"os" "os"
"path/filepath"
"dagger.io/go/cmd/dagger/logger" "dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state" "dagger.io/go/dagger/state"
@ -24,7 +23,7 @@ var initCmd = &cobra.Command{
lg := logger.New() lg := logger.New()
ctx := lg.WithContext(cmd.Context()) ctx := lg.WithContext(cmd.Context())
dir := viper.GetString("environment") dir := viper.GetString("workspace")
if dir == "" { if dir == "" {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
@ -36,29 +35,13 @@ var initCmd = &cobra.Command{
dir = cwd dir = cwd
} }
var name string _, err := state.Init(ctx, dir)
if len(args) > 0 {
name = args[0]
} else {
name = getNewEnvironmentName(dir)
}
_, err := state.Init(ctx, dir, name)
if err != nil { if err != nil {
lg.Fatal().Err(err).Msg("failed to initialize") lg.Fatal().Err(err).Msg("failed to initialize workspace")
} }
}, },
} }
func getNewEnvironmentName(dir string) string {
dirName := filepath.Base(dir)
if dirName == "/" {
return "root"
}
return dirName
}
func init() { func init() {
if err := viper.BindPFlags(initCmd.Flags()); err != nil { if err := viper.BindPFlags(initCmd.Flags()); err != nil {
panic(err) panic(err)

View File

@ -1,6 +1,10 @@
package input package input
import ( import (
"path/filepath"
"strings"
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger" "dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state" "dagger.io/go/dagger/state"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -22,7 +26,24 @@ var dirCmd = &cobra.Command{
lg := logger.New() lg := logger.New()
ctx := lg.WithContext(cmd.Context()) ctx := lg.WithContext(cmd.Context())
updateEnvironmentInput(ctx, args[0], state.DirInput(args[1], []string{})) p, err := filepath.Abs(args[1])
if err != nil {
lg.Fatal().Err(err).Str("path", args[1]).Msg("unable to resolve path")
}
workspace := common.CurrentWorkspace(ctx)
if !strings.HasPrefix(p, workspace.Path) {
lg.Fatal().Err(err).Str("path", args[1]).Msg("dir is outside the workspace")
}
p, err = filepath.Rel(workspace.Path, p)
if err != nil {
lg.Fatal().Err(err).Str("path", args[1]).Msg("unable to resolve path")
}
if !strings.HasPrefix(p, ".") {
p = "./" + p
}
updateEnvironmentInput(ctx, args[0], state.DirInput(p, []string{}))
}, },
} }

View File

@ -31,7 +31,8 @@ var listCmd = &cobra.Command{
lg := logger.New() lg := logger.New()
ctx := lg.WithContext(cmd.Context()) ctx := lg.WithContext(cmd.Context())
environment := common.GetCurrentEnvironmentState(ctx) workspace := common.CurrentWorkspace(ctx)
environment := common.CurrentEnvironmentState(ctx, workspace)
lg = lg.With(). lg = lg.With().
Str("environment", environment.Name). Str("environment", environment.Name).

View File

@ -35,10 +35,11 @@ func init() {
func updateEnvironmentInput(ctx context.Context, target string, input state.Input) { func updateEnvironmentInput(ctx context.Context, target string, input state.Input) {
lg := log.Ctx(ctx) lg := log.Ctx(ctx)
st := common.GetCurrentEnvironmentState(ctx) workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
st.SetInput(target, input) st.SetInput(target, input)
if err := state.Save(ctx, st); err != nil { if err := workspace.Save(ctx, st); err != nil {
lg.Fatal().Err(err).Str("environment", st.Name).Msg("cannot update environment") lg.Fatal().Err(err).Str("environment", st.Name).Msg("cannot update environment")
} }
} }

View File

@ -3,7 +3,6 @@ package input
import ( import (
"dagger.io/go/cmd/dagger/cmd/common" "dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger" "dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -23,10 +22,11 @@ var unsetCmd = &cobra.Command{
lg := logger.New() lg := logger.New()
ctx := lg.WithContext(cmd.Context()) ctx := lg.WithContext(cmd.Context())
st := common.GetCurrentEnvironmentState(ctx) workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
st.RemoveInputs(args[0]) st.RemoveInputs(args[0])
if err := state.Save(ctx, st); err != nil { if err := workspace.Save(ctx, st); err != nil {
lg.Fatal().Err(err).Str("environment", st.Name).Msg("cannot update environment") lg.Fatal().Err(err).Str("environment", st.Name).Msg("cannot update environment")
} }
lg.Info().Str("environment", st.Name).Msg("updated environment") lg.Info().Str("environment", st.Name).Msg("updated environment")

View File

@ -1,8 +1,6 @@
package cmd package cmd
import ( import (
"context"
"errors"
"fmt" "fmt"
"os" "os"
"os/user" "os/user"
@ -10,9 +8,8 @@ import (
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger" "dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -32,21 +29,8 @@ var listCmd = &cobra.Command{
lg := logger.New() lg := logger.New()
ctx := lg.WithContext(cmd.Context()) ctx := lg.WithContext(cmd.Context())
var ( workspace := common.CurrentWorkspace(ctx)
workspace = viper.GetString("workspace") environments, err := workspace.List(ctx)
err error
)
if workspace == "" {
workspace, err = state.CurrentWorkspace(ctx)
if err != nil {
lg.
Fatal().
Err(err).
Msg("failed to determine current workspace")
}
}
environments, err := state.List(ctx, workspace)
if err != nil { if err != nil {
lg. lg.
Fatal(). Fatal().
@ -54,34 +38,15 @@ var listCmd = &cobra.Command{
Msg("cannot list environments") Msg("cannot list environments")
} }
environmentPath := getCurrentEnvironmentPath(ctx)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent)
defer w.Flush() defer w.Flush()
for _, e := range environments { for _, e := range environments {
line := fmt.Sprintf("%s\t%s\t", e.Name, formatPath(e.Path)) line := fmt.Sprintf("%s\t%s\t", e.Name, formatPath(e.Path))
if e.Path == environmentPath {
line = fmt.Sprintf("%s- active environment", line)
}
fmt.Fprintln(w, line) fmt.Fprintln(w, line)
} }
}, },
} }
func getCurrentEnvironmentPath(ctx context.Context) string {
lg := log.Ctx(ctx)
st, err := state.Current(ctx)
if err != nil {
// Ignore error if not initialized
if errors.Is(err, state.ErrNotInit) {
return ""
}
lg.Fatal().Err(err).Msg("failed to load current environment")
}
return st.Path
}
func formatPath(p string) string { func formatPath(p string) string {
usr, err := user.Current() usr, err := user.Current()
if err != nil { if err != nil {

42
cmd/dagger/cmd/new.go Normal file
View File

@ -0,0 +1,42 @@
package cmd
import (
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var newCmd = &cobra.Command{
Use: "new",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
// Fix Viper bug for duplicate flags:
// https://github.com/spf13/viper/issues/233
if err := viper.BindPFlags(cmd.Flags()); err != nil {
panic(err)
}
},
Run: func(cmd *cobra.Command, args []string) {
lg := logger.New()
ctx := lg.WithContext(cmd.Context())
workspace := common.CurrentWorkspace(ctx)
if viper.GetString("environment") != "" {
lg.
Fatal().
Msg("cannot use option -e,--environment for this command")
}
name := args[0]
if _, err := workspace.Create(ctx, name); err != nil {
lg.Fatal().Err(err).Msg("failed to create environment")
}
},
}
func init() {
if err := viper.BindPFlags(newCmd.Flags()); err != nil {
panic(err)
}
}

View File

@ -30,7 +30,8 @@ var queryCmd = &cobra.Command{
cueOpts := parseQueryFlags() cueOpts := parseQueryFlags()
state := common.GetCurrentEnvironmentState(ctx) workspace := common.CurrentWorkspace(ctx)
state := common.CurrentEnvironmentState(ctx, workspace)
lg = lg.With(). lg = lg.With().
Str("environment", state.Name). Str("environment", state.Name).

View File

@ -34,6 +34,7 @@ func init() {
rootCmd.AddCommand( rootCmd.AddCommand(
initCmd, initCmd,
newCmd,
computeCmd, computeCmd,
listCmd, listCmd,
queryCmd, queryCmd,

View File

@ -3,7 +3,6 @@ package cmd
import ( import (
"dagger.io/go/cmd/dagger/cmd/common" "dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger" "dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -24,11 +23,12 @@ var upCmd = &cobra.Command{
lg := logger.New() lg := logger.New()
ctx := lg.WithContext(cmd.Context()) ctx := lg.WithContext(cmd.Context())
st := common.GetCurrentEnvironmentState(ctx) workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
result := common.EnvironmentUp(ctx, st, viper.GetBool("no-cache")) result := common.EnvironmentUp(ctx, st, viper.GetBool("no-cache"))
st.Computed = result.Computed().JSON().PrettyString() st.Computed = result.Computed().JSON().PrettyString()
if err := state.Save(ctx, st); err != nil { if err := workspace.Save(ctx, st); err != nil {
lg.Fatal().Err(err).Msg("failed to update environment") lg.Fatal().Err(err).Msg("failed to update environment")
} }
}, },

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"cuelang.org/go/cue" "cuelang.org/go/cue"
@ -90,7 +91,10 @@ func (dir dirInput) Compile(state *State) (*compiler.Value, error) {
p := dir.Path p := dir.Path
if !filepath.IsAbs(p) { if !filepath.IsAbs(p) {
p = filepath.Clean(path.Join(state.Path, p)) p = filepath.Clean(path.Join(state.Workspace, dir.Path))
}
if !strings.HasPrefix(p, state.Workspace) {
return nil, fmt.Errorf("%q is outside the workspace", dir.Path)
} }
llb := fmt.Sprintf( llb := fmt.Sprintf(

View File

@ -1,10 +1,15 @@
package state package state
import "path"
// Contents of an environment serialized to a file // Contents of an environment serialized to a file
type State struct { type State struct {
// State path // State path
Path string `yaml:"-"` Path string `yaml:"-"`
// Workspace path
Workspace string `yaml:"-"`
// Human-friendly environment name. // Human-friendly environment name.
// A environment may have more than one name. // A environment may have more than one name.
// FIXME: store multiple names? // FIXME: store multiple names?
@ -20,7 +25,7 @@ type State struct {
// Cue module containing the environment plan // Cue module containing the environment plan
// The input's top-level artifact is used as a module directory. // The input's top-level artifact is used as a module directory.
func (s *State) PlanSource() Input { func (s *State) PlanSource() Input {
return DirInput(s.Path, []string{"*.cue", "cue.mod"}) return DirInput(path.Join(s.Path, planDir), []string{"*.cue", "cue.mod"})
} }
func (s *State) SetInput(key string, value Input) error { func (s *State) SetInput(key string, value Input) error {

View File

@ -7,26 +7,32 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"dagger.io/go/dagger/keychain" "dagger.io/go/dagger/keychain"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
var ( var (
ErrNotInit = errors.New("not initialized") ErrNotInit = errors.New("not initialized")
ErrAlreadyInit = errors.New("already initialized") ErrAlreadyInit = errors.New("already initialized")
ErrNoCurrentWorkspace = errors.New("not in a git directory") ErrNotExist = errors.New("environment doesn't exist")
ErrExist = errors.New("environment already exists")
) )
const ( const (
daggerDir = ".dagger" daggerDir = ".dagger"
envDir = "env"
stateDir = "state" stateDir = "state"
planDir = "plan"
manifestFile = "values.yaml" manifestFile = "values.yaml"
computedFile = "computed.json" computedFile = "computed.json"
) )
func Init(ctx context.Context, dir, name string) (*State, error) { type Workspace struct {
Path string
}
func Init(ctx context.Context, dir string) (*Workspace, error) {
root := path.Join(dir, daggerDir) root := path.Join(dir, daggerDir)
if err := os.Mkdir(root, 0755); err != nil { if err := os.Mkdir(root, 0755); err != nil {
if errors.Is(err, os.ErrExist) { if errors.Is(err, os.ErrExist) {
@ -34,41 +40,31 @@ func Init(ctx context.Context, dir, name string) (*State, error) {
} }
return nil, err return nil, err
} }
manifestPath := path.Join(dir, daggerDir, manifestFile) return &Workspace{
Path: root,
st := &State{ }, nil
Path: dir,
Name: name,
}
data, err := yaml.Marshal(st)
if err != nil {
return nil, err
}
key, err := keychain.Default(ctx)
if err != nil {
return nil, err
}
encrypted, err := keychain.Encrypt(ctx, manifestPath, data, key)
if err != nil {
return nil, err
}
if err := os.WriteFile(manifestPath, encrypted, 0600); err != nil {
return nil, err
}
err = os.WriteFile(
path.Join(root, ".gitignore"),
[]byte("# dagger state\nstate/**\n"),
0600,
)
if err != nil {
return nil, err
}
return st, nil
} }
func Current(ctx context.Context) (*State, error) { func Open(ctx context.Context, dir string) (*Workspace, error) {
_, err := os.Stat(path.Join(dir, daggerDir))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotInit
}
return nil, err
}
root, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
return &Workspace{
Path: root,
}, nil
}
func Current(ctx context.Context) (*Workspace, error) {
current, err := os.Getwd() current, err := os.Getwd()
if err != nil { if err != nil {
return nil, err return nil, err
@ -90,36 +86,63 @@ func Current(ctx context.Context) (*State, error) {
return nil, ErrNotInit return nil, ErrNotInit
} }
func Open(ctx context.Context, dir string) (*State, error) { func (w *Workspace) envPath(name string) string {
_, err := os.Stat(path.Join(dir, daggerDir)) return path.Join(w.Path, daggerDir, envDir, name)
}
func (w *Workspace) List(ctx context.Context) ([]*State, error) {
var (
environments = []*State{}
err error
)
files, err := os.ReadDir(path.Join(w.Path, daggerDir, envDir))
if err != nil { if err != nil {
return nil, err
}
for _, f := range files {
if !f.IsDir() {
continue
}
st, err := w.Get(ctx, f.Name())
if err != nil {
return nil, err
}
environments = append(environments, st)
}
return environments, nil
}
func (w *Workspace) Get(ctx context.Context, name string) (*State, error) {
envPath, err := filepath.Abs(w.envPath(name))
if err != nil {
return nil, err
}
if _, err := os.Stat(envPath); err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotInit return nil, ErrNotExist
} }
return nil, err return nil, err
} }
root, err := filepath.Abs(dir) manifest, err := os.ReadFile(path.Join(envPath, manifestFile))
if err != nil { if err != nil {
return nil, err return nil, err
} }
manifest, err = keychain.Decrypt(ctx, manifest)
data, err := os.ReadFile(path.Join(root, daggerDir, manifestFile))
if err != nil {
return nil, err
}
data, err = keychain.Decrypt(ctx, data)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to decrypt state: %w", err) return nil, fmt.Errorf("unable to decrypt state: %w", err)
} }
var st State var st State
if err := yaml.Unmarshal(data, &st); err != nil { if err := yaml.Unmarshal(manifest, &st); err != nil {
return nil, err return nil, err
} }
st.Path = root st.Path = envPath
st.Workspace = w.Path
computed, err := os.ReadFile(path.Join(root, daggerDir, stateDir, computedFile)) computed, err := os.ReadFile(path.Join(envPath, stateDir, computedFile))
if err == nil { if err == nil {
st.Computed = string(computed) st.Computed = string(computed)
} }
@ -127,13 +150,13 @@ func Open(ctx context.Context, dir string) (*State, error) {
return &st, nil return &st, nil
} }
func Save(ctx context.Context, st *State) error { func (w *Workspace) Save(ctx context.Context, st *State) error {
data, err := yaml.Marshal(st) data, err := yaml.Marshal(st)
if err != nil { if err != nil {
return err return err
} }
manifestPath := path.Join(st.Path, daggerDir, manifestFile) manifestPath := path.Join(st.Path, manifestFile)
encrypted, err := keychain.Reencrypt(ctx, manifestPath, data) encrypted, err := keychain.Reencrypt(ctx, manifestPath, data)
if err != nil { if err != nil {
@ -144,7 +167,7 @@ func Save(ctx context.Context, st *State) error {
} }
if st.Computed != "" { if st.Computed != "" {
state := path.Join(st.Path, daggerDir, stateDir) state := path.Join(st.Path, stateDir)
if err := os.MkdirAll(state, 0755); err != nil { if err := os.MkdirAll(state, 0755); err != nil {
return err return err
} }
@ -160,74 +183,59 @@ func Save(ctx context.Context, st *State) error {
return nil return nil
} }
func CurrentWorkspace(ctx context.Context) (string, error) { func (w *Workspace) Create(ctx context.Context, name string) (*State, error) {
current, err := os.Getwd() envPath, err := filepath.Abs(w.envPath(name))
if err != nil { if err != nil {
return "", err return nil, err
} }
// Walk every parent directory to find .dagger // Environment directory
for { if err := os.MkdirAll(envPath, 0755); err != nil {
_, err := os.Stat(path.Join(current, ".git")) if errors.Is(err, os.ErrExist) {
if err == nil { return nil, ErrExist
return current, nil
} }
parent := filepath.Dir(current) return nil, err
if parent == current {
break
}
current = parent
} }
return "", ErrNoCurrentWorkspace // Plan directory
} if err := os.Mkdir(path.Join(envPath, planDir), 0755); err != nil {
if errors.Is(err, os.ErrExist) {
return nil, ErrExist
}
return nil, err
}
func List(ctx context.Context, workspace string) ([]*State, error) { manifestPath := path.Join(envPath, manifestFile)
var (
environments = []*State{} st := &State{
err error Path: envPath,
Workspace: w.Path,
Name: name,
}
data, err := yaml.Marshal(st)
if err != nil {
return nil, err
}
key, err := keychain.Default(ctx)
if err != nil {
return nil, err
}
encrypted, err := keychain.Encrypt(ctx, manifestPath, data, key)
if err != nil {
return nil, err
}
if err := os.WriteFile(manifestPath, encrypted, 0600); err != nil {
return nil, err
}
err = os.WriteFile(
path.Join(envPath, ".gitignore"),
[]byte("# dagger state\nstate/**\n"),
0600,
) )
workspace, err = filepath.Abs(workspace)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = filepath.WalkDir(workspace, func(p string, info os.DirEntry, err error) error { return st, nil
// Ignore errors while we walk
if err != nil {
return nil
}
// Skip regular files
if !info.IsDir() {
return nil
}
// Skip non-dagger directories
if info.Name() != daggerDir {
// Caveat: limit traversal to a depth of 10 (arbitrary)
relPath := strings.TrimPrefix(p, workspace)
if strings.Count(relPath, string(os.PathSeparator)) > 10 {
return filepath.SkipDir
}
// Otherwise, continue traversing
return nil
}
st, err := Open(ctx, filepath.Dir(p))
if err != nil {
return err
}
environments = append(environments, st)
return nil
})
if err != nil {
return nil, err
}
return environments, nil
} }