diff --git a/cmd/dagger/cmd/common/common.go b/cmd/dagger/cmd/common/common.go index b7c87cee..4c0aae01 100644 --- a/cmd/dagger/cmd/common/common.go +++ b/cmd/dagger/cmd/common/common.go @@ -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`). diff --git a/cmd/dagger/cmd/init.go b/cmd/dagger/cmd/init.go index 1bc16633..c47346f7 100644 --- a/cmd/dagger/cmd/init.go +++ b/cmd/dagger/cmd/init.go @@ -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) diff --git a/cmd/dagger/cmd/input/dir.go b/cmd/dagger/cmd/input/dir.go index f18e44f1..0e23ccee 100644 --- a/cmd/dagger/cmd/input/dir.go +++ b/cmd/dagger/cmd/input/dir.go @@ -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{})) }, } diff --git a/cmd/dagger/cmd/input/list.go b/cmd/dagger/cmd/input/list.go index 22929be0..bc1faeb8 100644 --- a/cmd/dagger/cmd/input/list.go +++ b/cmd/dagger/cmd/input/list.go @@ -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). diff --git a/cmd/dagger/cmd/input/root.go b/cmd/dagger/cmd/input/root.go index f035fda8..e9cf2e75 100644 --- a/cmd/dagger/cmd/input/root.go +++ b/cmd/dagger/cmd/input/root.go @@ -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") } } diff --git a/cmd/dagger/cmd/input/unset.go b/cmd/dagger/cmd/input/unset.go index 248aae7a..9b643c0b 100644 --- a/cmd/dagger/cmd/input/unset.go +++ b/cmd/dagger/cmd/input/unset.go @@ -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") diff --git a/cmd/dagger/cmd/list.go b/cmd/dagger/cmd/list.go index fd6b30a1..4e1fb8d0 100644 --- a/cmd/dagger/cmd/list.go +++ b/cmd/dagger/cmd/list.go @@ -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 { diff --git a/cmd/dagger/cmd/new.go b/cmd/dagger/cmd/new.go new file mode 100644 index 00000000..c2c019c0 --- /dev/null +++ b/cmd/dagger/cmd/new.go @@ -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) + } +} diff --git a/cmd/dagger/cmd/query.go b/cmd/dagger/cmd/query.go index 62981608..2c689d5e 100644 --- a/cmd/dagger/cmd/query.go +++ b/cmd/dagger/cmd/query.go @@ -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). diff --git a/cmd/dagger/cmd/root.go b/cmd/dagger/cmd/root.go index d3cc7cd8..77006d9c 100644 --- a/cmd/dagger/cmd/root.go +++ b/cmd/dagger/cmd/root.go @@ -34,6 +34,7 @@ func init() { rootCmd.AddCommand( initCmd, + newCmd, computeCmd, listCmd, queryCmd, diff --git a/cmd/dagger/cmd/up.go b/cmd/dagger/cmd/up.go index 808313e6..440216c5 100644 --- a/cmd/dagger/cmd/up.go +++ b/cmd/dagger/cmd/up.go @@ -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") } }, diff --git a/dagger/state/input.go b/dagger/state/input.go index 9c34bdd6..4c06e47e 100644 --- a/dagger/state/input.go +++ b/dagger/state/input.go @@ -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( diff --git a/dagger/state/state.go b/dagger/state/state.go index 49480ebd..becf5d1a 100644 --- a/dagger/state/state.go +++ b/dagger/state/state.go @@ -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 { diff --git a/dagger/state/store.go b/dagger/state/store.go index d219942c..b6699e5d 100644 --- a/dagger/state/store.go +++ b/dagger/state/store.go @@ -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 }