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"
)
func GetCurrentEnvironmentState(ctx context.Context) *state.State {
func CurrentWorkspace(ctx context.Context) *state.Workspace {
lg := log.Ctx(ctx)
// If no environment name has been given, look for the current environment
environment := viper.GetString("environment")
if environment == "" {
st, err := state.Current(ctx)
if workspacePath := viper.GetString("workspace"); workspacePath != "" {
workspace, err := state.Open(ctx, workspacePath)
if err != nil {
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 {
lg.
Fatal().
@ -25,38 +49,33 @@ func GetCurrentEnvironmentState(ctx context.Context) *state.State {
return st
}
// At this point, it must be an environment name
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)
environments, err := workspace.List(ctx)
if err != nil {
lg.
Fatal().
Err(err).
Msg("failed to list environments")
}
for _, e := range environments {
if e.Name == environment {
return e
}
if len(environments) == 0 {
lg.
Fatal().
Msg("no environments")
}
lg.
Fatal().
Str("environment", environment).
Msg("environment not found")
if len(environments) > 1 {
envNames := []string{}
for _, e := range environments {
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`).

View File

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

View File

@ -1,6 +1,10 @@
package input
import (
"path/filepath"
"strings"
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra"
@ -22,7 +26,24 @@ var dirCmd = &cobra.Command{
lg := logger.New()
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()
ctx := lg.WithContext(cmd.Context())
environment := common.GetCurrentEnvironmentState(ctx)
workspace := common.CurrentWorkspace(ctx)
environment := common.CurrentEnvironmentState(ctx, workspace)
lg = lg.With().
Str("environment", environment.Name).

View File

@ -35,10 +35,11 @@ func init() {
func updateEnvironmentInput(ctx context.Context, target string, input state.Input) {
lg := log.Ctx(ctx)
st := common.GetCurrentEnvironmentState(ctx)
workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
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")
}
}

View File

@ -3,7 +3,6 @@ package input
import (
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -23,10 +22,11 @@ var unsetCmd = &cobra.Command{
lg := logger.New()
ctx := lg.WithContext(cmd.Context())
st := common.GetCurrentEnvironmentState(ctx)
workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
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.Info().Str("environment", st.Name).Msg("updated environment")

View File

@ -1,8 +1,6 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/user"
@ -10,9 +8,8 @@ import (
"strings"
"text/tabwriter"
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -32,21 +29,8 @@ var listCmd = &cobra.Command{
lg := logger.New()
ctx := lg.WithContext(cmd.Context())
var (
workspace = viper.GetString("workspace")
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)
workspace := common.CurrentWorkspace(ctx)
environments, err := workspace.List(ctx)
if err != nil {
lg.
Fatal().
@ -54,34 +38,15 @@ var listCmd = &cobra.Command{
Msg("cannot list environments")
}
environmentPath := getCurrentEnvironmentPath(ctx)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent)
defer w.Flush()
for _, e := range environments {
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)
}
},
}
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 {
usr, err := user.Current()
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()
state := common.GetCurrentEnvironmentState(ctx)
workspace := common.CurrentWorkspace(ctx)
state := common.CurrentEnvironmentState(ctx, workspace)
lg = lg.With().
Str("environment", state.Name).

View File

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

View File

@ -3,7 +3,6 @@ package cmd
import (
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -24,11 +23,12 @@ var upCmd = &cobra.Command{
lg := logger.New()
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"))
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")
}
},

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"path"
"path/filepath"
"strings"
"cuelang.org/go/cue"
@ -90,7 +91,10 @@ func (dir dirInput) Compile(state *State) (*compiler.Value, error) {
p := dir.Path
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(

View File

@ -1,10 +1,15 @@
package state
import "path"
// Contents of an environment serialized to a file
type State struct {
// State path
Path string `yaml:"-"`
// Workspace path
Workspace string `yaml:"-"`
// Human-friendly environment name.
// A environment may have more than one name.
// FIXME: store multiple names?
@ -20,7 +25,7 @@ type State struct {
// Cue module containing the environment plan
// The input's top-level artifact is used as a module directory.
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 {

View File

@ -7,26 +7,32 @@ import (
"os"
"path"
"path/filepath"
"strings"
"dagger.io/go/dagger/keychain"
"gopkg.in/yaml.v3"
)
var (
ErrNotInit = errors.New("not initialized")
ErrAlreadyInit = errors.New("already initialized")
ErrNoCurrentWorkspace = errors.New("not in a git directory")
ErrNotInit = errors.New("not initialized")
ErrAlreadyInit = errors.New("already initialized")
ErrNotExist = errors.New("environment doesn't exist")
ErrExist = errors.New("environment already exists")
)
const (
daggerDir = ".dagger"
envDir = "env"
stateDir = "state"
planDir = "plan"
manifestFile = "values.yaml"
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)
if err := os.Mkdir(root, 0755); err != nil {
if errors.Is(err, os.ErrExist) {
@ -34,41 +40,31 @@ func Init(ctx context.Context, dir, name string) (*State, error) {
}
return nil, err
}
manifestPath := path.Join(dir, daggerDir, manifestFile)
st := &State{
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
return &Workspace{
Path: root,
}, 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()
if err != nil {
return nil, err
@ -90,36 +86,63 @@ func Current(ctx context.Context) (*State, error) {
return nil, ErrNotInit
}
func Open(ctx context.Context, dir string) (*State, error) {
_, err := os.Stat(path.Join(dir, daggerDir))
func (w *Workspace) envPath(name string) string {
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 {
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) {
return nil, ErrNotInit
return nil, ErrNotExist
}
return nil, err
}
root, err := filepath.Abs(dir)
manifest, err := os.ReadFile(path.Join(envPath, manifestFile))
if err != nil {
return nil, err
}
data, err := os.ReadFile(path.Join(root, daggerDir, manifestFile))
if err != nil {
return nil, err
}
data, err = keychain.Decrypt(ctx, data)
manifest, err = keychain.Decrypt(ctx, manifest)
if err != nil {
return nil, fmt.Errorf("unable to decrypt state: %w", err)
}
var st State
if err := yaml.Unmarshal(data, &st); err != nil {
if err := yaml.Unmarshal(manifest, &st); err != nil {
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 {
st.Computed = string(computed)
}
@ -127,13 +150,13 @@ func Open(ctx context.Context, dir string) (*State, error) {
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)
if err != nil {
return err
}
manifestPath := path.Join(st.Path, daggerDir, manifestFile)
manifestPath := path.Join(st.Path, manifestFile)
encrypted, err := keychain.Reencrypt(ctx, manifestPath, data)
if err != nil {
@ -144,7 +167,7 @@ func Save(ctx context.Context, st *State) error {
}
if st.Computed != "" {
state := path.Join(st.Path, daggerDir, stateDir)
state := path.Join(st.Path, stateDir)
if err := os.MkdirAll(state, 0755); err != nil {
return err
}
@ -160,74 +183,59 @@ func Save(ctx context.Context, st *State) error {
return nil
}
func CurrentWorkspace(ctx context.Context) (string, error) {
current, err := os.Getwd()
func (w *Workspace) Create(ctx context.Context, name string) (*State, error) {
envPath, err := filepath.Abs(w.envPath(name))
if err != nil {
return "", err
return nil, err
}
// Walk every parent directory to find .dagger
for {
_, err := os.Stat(path.Join(current, ".git"))
if err == nil {
return current, nil
// Environment directory
if err := os.MkdirAll(envPath, 0755); err != nil {
if errors.Is(err, os.ErrExist) {
return nil, ErrExist
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
return nil, err
}
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) {
var (
environments = []*State{}
err error
manifestPath := path.Join(envPath, manifestFile)
st := &State{
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 {
return nil, err
}
err = filepath.WalkDir(workspace, func(p string, info os.DirEntry, err error) error {
// 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
return st, nil
}