From b77d1a1e3e31d996dc25de4b0a18f542e59bfd42 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Fri, 7 May 2021 14:45:15 -0700 Subject: [PATCH] gitflow ux Signed-off-by: Andrea Luzzardi --- .dagger/env/dev-stdlib/adhoc.cue | 44 --- .dagger/env/dev/main.cue | 69 ----- .dagger/env/hello-world | 1 - .dagger/env/sandbox/sandbox.cue | 21 -- cmd/dagger/cmd/common/common.go | 72 +++-- cmd/dagger/cmd/compute.go | 22 +- cmd/dagger/cmd/init.go | 66 +++++ cmd/dagger/cmd/input/container.go | 4 +- cmd/dagger/cmd/input/dir.go | 4 +- cmd/dagger/cmd/input/git.go | 4 +- cmd/dagger/cmd/input/json.go | 4 +- cmd/dagger/cmd/input/list.go | 10 +- cmd/dagger/cmd/input/root.go | 16 +- cmd/dagger/cmd/input/text.go | 4 +- cmd/dagger/cmd/input/unset.go | 15 +- cmd/dagger/cmd/input/yaml.go | 4 +- cmd/dagger/cmd/list.go | 73 +++-- cmd/dagger/cmd/new.go | 148 ----------- cmd/dagger/cmd/plan/dir.go | 33 --- cmd/dagger/cmd/plan/file.go | 31 --- cmd/dagger/cmd/plan/git.go | 43 --- cmd/dagger/cmd/plan/package.go | 31 --- cmd/dagger/cmd/plan/root.go | 42 --- cmd/dagger/cmd/query.go | 10 +- cmd/dagger/cmd/root.go | 5 +- cmd/dagger/cmd/up.go | 15 +- dagger/client.go | 3 +- dagger/compiler/json.go | 2 +- dagger/environment.go | 20 +- dagger/{input_test.go => environment_test.go} | 9 +- dagger/{ => state}/input.go | 63 ++--- dagger/{ => state}/state.go | 32 +-- dagger/state/store.go | 205 ++++++++++++++ dagger/state/store_test.go | 39 +++ dagger/store.go | 250 ------------------ dagger/store_test.go | 123 --------- go.mod | 1 - 37 files changed, 495 insertions(+), 1043 deletions(-) delete mode 100644 .dagger/env/dev-stdlib/adhoc.cue delete mode 100644 .dagger/env/dev/main.cue delete mode 120000 .dagger/env/hello-world delete mode 100644 .dagger/env/sandbox/sandbox.cue create mode 100644 cmd/dagger/cmd/init.go delete mode 100644 cmd/dagger/cmd/new.go delete mode 100644 cmd/dagger/cmd/plan/dir.go delete mode 100644 cmd/dagger/cmd/plan/file.go delete mode 100644 cmd/dagger/cmd/plan/git.go delete mode 100644 cmd/dagger/cmd/plan/package.go delete mode 100644 cmd/dagger/cmd/plan/root.go rename dagger/{input_test.go => environment_test.go} (61%) rename dagger/{ => state}/input.go (77%) rename dagger/{ => state}/state.go (58%) create mode 100644 dagger/state/store.go create mode 100644 dagger/state/store_test.go delete mode 100644 dagger/store.go delete mode 100644 dagger/store_test.go diff --git a/.dagger/env/dev-stdlib/adhoc.cue b/.dagger/env/dev-stdlib/adhoc.cue deleted file mode 100644 index 41f5a493..00000000 --- a/.dagger/env/dev-stdlib/adhoc.cue +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "dagger.io/dagger/op" -) - -// Reproduce inline issue. -// See https://github.com/dagger/dagger/issues/395 -test: adhoc: repro395: { - good: { - // This field is correctly computed because its intermediary pipeline is not inlined. - hello: sayHello.message - - // Intermediary pipeline cannot be inlined: it must be visible in a field - sayHello: { - message: { - string - #up: [ - op.#FetchContainer & { ref: "alpine" }, - op.#Exec & { - args: ["sh", "-c", "echo hello > /message"] - }, - op.#Export & { source: "/message", format: "string" }, - ] - } - } - } - bad: { - // This field is NOT correctly computed because its intermediary pipeline is inlined. - hello: { - message: { - string - #up: [ - op.#FetchContainer & { ref: "alpine" }, - op.#Exec & { - args: ["sh", "-c", "echo hello > /message"] - }, - op.#Export & { source: "/message", format: "string" }, - ] - } - }.message - - } -} diff --git a/.dagger/env/dev/main.cue b/.dagger/env/dev/main.cue deleted file mode 100644 index 8d7392fe..00000000 --- a/.dagger/env/dev/main.cue +++ /dev/null @@ -1,69 +0,0 @@ -// A dagger workflow to develop dagger -package main - -import ( - "dagger.io/dagger" - "dagger.io/os" - "dagger.io/alpine" - "dagger.io/docker" - "dagger.io/go" -) - -// Dagger source code -source: dagger.#Artifact - - -test: { - // Go unit tests - unit: { - logs: (os.#File & { - from: build.ctr - path: "/test.log" - read: format: "string" - }).read.data - } - - // Full suite of bats integration tests - integration: { - // FIXME - } -} - -// Build the dagger binaries -build: { - ctr: go.#Container & { - "source": source - setup: [ - "apk add --no-cache file", - ] - command: """ - go test -v ./... > /test.log - go build -o /binaries/ ./cmd/... > /build.log - """ - } - - binaries: docker.#Container & { - image: ctr - outputDir: "/binaries" - } - - logs: (os.#File & { - from: ctr - path: "/build.log" - read: format: "string" - }).read.data -} - - -// Execute `dagger help` -usage: docker.#Container & { - image: alpine.#Image - - command: "dagger help" - - volume: binaries: { - from: build.binaries - dest: "/usr/local/dagger/bin/" - } - shell: search: "/usr/local/dagger/bin": true -} diff --git a/.dagger/env/hello-world b/.dagger/env/hello-world deleted file mode 120000 index a69ee1e0..00000000 --- a/.dagger/env/hello-world +++ /dev/null @@ -1 +0,0 @@ -../../examples/hello-world \ No newline at end of file diff --git a/.dagger/env/sandbox/sandbox.cue b/.dagger/env/sandbox/sandbox.cue deleted file mode 100644 index 42e7e293..00000000 --- a/.dagger/env/sandbox/sandbox.cue +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "dagger.io/docker" - "dagger.io/os" -) - -let ctr = docker.#Container & { - command: "echo 'hello world!' > /etc/motd" -} - -motd: (os.#File & { - from: ctr - path: "/etc/motd" - read: format: "string" -}).read.data - -etc: (os.#Dir & { - from: ctr - path: "/etc" -}).read.tree diff --git a/cmd/dagger/cmd/common/common.go b/cmd/dagger/cmd/common/common.go index 2b10a382..b928cffa 100644 --- a/cmd/dagger/cmd/common/common.go +++ b/cmd/dagger/cmd/common/common.go @@ -2,65 +2,79 @@ package common import ( "context" + "errors" "os" "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) -func GetCurrentEnvironmentState(ctx context.Context, store *dagger.Store) *dagger.EnvironmentState { +func GetCurrentEnvironmentState(ctx context.Context) *state.State { lg := log.Ctx(ctx) - environmentName := viper.GetString("environment") - if environmentName != "" { - st, err := store.LookupEnvironmentByName(ctx, environmentName) + // 1) If no environment name has been given, look for the current environment + environment := viper.GetString("environment") + if environment == "" { + st, err := state.Current(ctx) if err != nil { lg. Fatal(). Err(err). - Str("environmentName", environmentName). - Msg("failed to lookup environment by name") + Msg("failed to load environment") } return st } - wd, err := os.Getwd() - if err != nil { - lg.Fatal().Err(err).Msg("cannot get current working directory") + // 2) Check if it's an environment path (can be opened directly) + st, err := state.Open(ctx, environment) + if err == nil { + return st } - st, err := store.LookupEnvironmentByPath(ctx, wd) - if err != nil { + if !errors.Is(err, os.ErrNotExist) { lg. Fatal(). Err(err). - Str("environmentPath", wd). - Msg("failed to lookup environment by path") + Str("environmentPath", environment). + Msg("failed to load environment") } - if len(st) == 0 { - lg. - Fatal(). - Err(err). - Str("environmentPath", wd). - Msg("no environments match the current directory") - } - if len(st) > 1 { - environments := []string{} - for _, s := range st { - environments = append(environments, s.Name) + + // At this point, it must be an environment name + workspace := viper.GetString("workspace") + 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 { lg. Fatal(). Err(err). - Str("environmentPath", wd). - Strs("environments", environments). - Msg("multiple environments match the current directory, select one with `--environment`") + Msg("failed to list environments") } - return st[0] + for _, e := range environments { + if e.Name == environment { + return e + } + } + + lg. + Fatal(). + Str("environment", environment). + Msg("environment not found") + + return nil } // Re-compute an environment (equivalent to `dagger up`). -func EnvironmentUp(ctx context.Context, state *dagger.EnvironmentState, noCache bool) *dagger.Environment { +func EnvironmentUp(ctx context.Context, state *state.State, noCache bool) *dagger.Environment { lg := log.Ctx(ctx) c, err := dagger.NewClient(ctx, "", noCache) diff --git a/cmd/dagger/cmd/compute.go b/cmd/dagger/cmd/compute.go index 1f15e4cb..c49e235e 100644 --- a/cmd/dagger/cmd/compute.go +++ b/cmd/dagger/cmd/compute.go @@ -10,12 +10,11 @@ import ( "cuelang.org/go/cue" "dagger.io/go/cmd/dagger/cmd/common" "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" "dagger.io/go/dagger/compiler" + "dagger.io/go/dagger/state" "go.mozilla.org/sops/v3" "go.mozilla.org/sops/v3/decrypt" - "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -36,16 +35,15 @@ var computeCmd = &cobra.Command{ lg := logger.New() ctx := lg.WithContext(cmd.Context()) - st := &dagger.EnvironmentState{ - ID: uuid.New().String(), - Name: "FIXME", - PlanSource: dagger.DirInput(args[0], []string{"*.cue", "cue.mod"}), + st := &state.State{ + Name: "FIXME", + Path: args[0], } for _, input := range viper.GetStringSlice("input-string") { parts := strings.SplitN(input, "=", 2) k, v := parts[0], parts[1] - err := st.SetInput(k, dagger.TextInput(v)) + err := st.SetInput(k, state.TextInput(v)) if err != nil { lg. Fatal(). @@ -58,7 +56,7 @@ var computeCmd = &cobra.Command{ for _, input := range viper.GetStringSlice("input-dir") { parts := strings.SplitN(input, "=", 2) k, v := parts[0], parts[1] - err := st.SetInput(k, dagger.DirInput(v, []string{})) + err := st.SetInput(k, state.DirInput(v, []string{})) if err != nil { lg. Fatal(). @@ -71,7 +69,7 @@ var computeCmd = &cobra.Command{ for _, input := range viper.GetStringSlice("input-git") { parts := strings.SplitN(input, "=", 2) k, v := parts[0], parts[1] - err := st.SetInput(k, dagger.GitInput(v, "", "")) + err := st.SetInput(k, state.GitInput(v, "", "")) if err != nil { lg. Fatal(). @@ -102,7 +100,7 @@ var computeCmd = &cobra.Command{ lg.Fatal().Msg("invalid json") } - err = st.SetInput("", dagger.JSONInput(string(content))) + err = st.SetInput("", state.JSONInput(string(content))) if err != nil { lg.Fatal().Err(err).Msg("failed to add input") } @@ -125,7 +123,7 @@ var computeCmd = &cobra.Command{ content = plaintext } - err = st.SetInput("", dagger.YAMLInput(string(content))) + err = st.SetInput("", state.YAMLInput(string(content))) if err != nil { lg.Fatal().Err(err).Msg("failed to add input") } @@ -143,7 +141,7 @@ var computeCmd = &cobra.Command{ } if len(content) > 0 { - err = st.SetInput(k, dagger.FileInput(v)) + err = st.SetInput(k, state.FileInput(v)) if err != nil { lg.Fatal().Err(err).Msg("failed to set input string") } diff --git a/cmd/dagger/cmd/init.go b/cmd/dagger/cmd/init.go new file mode 100644 index 00000000..1bc16633 --- /dev/null +++ b/cmd/dagger/cmd/init.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "os" + "path/filepath" + + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger/state" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var initCmd = &cobra.Command{ + Use: "init", + Args: cobra.MaximumNArgs(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()) + + dir := viper.GetString("environment") + if dir == "" { + cwd, err := os.Getwd() + if err != nil { + lg. + Fatal(). + Err(err). + Msg("failed to get current working dir") + } + dir = cwd + } + + var name string + if len(args) > 0 { + name = args[0] + } else { + name = getNewEnvironmentName(dir) + } + + _, err := state.Init(ctx, dir, name) + if err != nil { + lg.Fatal().Err(err).Msg("failed to initialize") + } + }, +} + +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/container.go b/cmd/dagger/cmd/input/container.go index 7b4a5be6..6ced900f 100644 --- a/cmd/dagger/cmd/input/container.go +++ b/cmd/dagger/cmd/input/container.go @@ -2,7 +2,7 @@ package input import ( "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -22,7 +22,7 @@ var containerCmd = &cobra.Command{ lg := logger.New() ctx := lg.WithContext(cmd.Context()) - updateEnvironmentInput(ctx, args[0], dagger.DockerInput(args[1])) + updateEnvironmentInput(ctx, args[0], state.DockerInput(args[1])) }, } diff --git a/cmd/dagger/cmd/input/dir.go b/cmd/dagger/cmd/input/dir.go index 0dd05073..f18e44f1 100644 --- a/cmd/dagger/cmd/input/dir.go +++ b/cmd/dagger/cmd/input/dir.go @@ -2,7 +2,7 @@ package input import ( "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -22,7 +22,7 @@ var dirCmd = &cobra.Command{ lg := logger.New() ctx := lg.WithContext(cmd.Context()) - updateEnvironmentInput(ctx, args[0], dagger.DirInput(args[1], []string{})) + updateEnvironmentInput(ctx, args[0], state.DirInput(args[1], []string{})) }, } diff --git a/cmd/dagger/cmd/input/git.go b/cmd/dagger/cmd/input/git.go index 384df2bf..f718b395 100644 --- a/cmd/dagger/cmd/input/git.go +++ b/cmd/dagger/cmd/input/git.go @@ -2,7 +2,7 @@ package input import ( "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -32,7 +32,7 @@ var gitCmd = &cobra.Command{ subDir = args[3] } - updateEnvironmentInput(ctx, args[0], dagger.GitInput(args[1], ref, subDir)) + updateEnvironmentInput(ctx, args[0], state.GitInput(args[1], ref, subDir)) }, } diff --git a/cmd/dagger/cmd/input/json.go b/cmd/dagger/cmd/input/json.go index 557a4ab8..508328bf 100644 --- a/cmd/dagger/cmd/input/json.go +++ b/cmd/dagger/cmd/input/json.go @@ -2,7 +2,7 @@ package input import ( "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -25,7 +25,7 @@ var jsonCmd = &cobra.Command{ updateEnvironmentInput( ctx, args[0], - dagger.JSONInput(readInput(ctx, args[1])), + state.JSONInput(readInput(ctx, args[1])), ) }, } diff --git a/cmd/dagger/cmd/input/list.go b/cmd/dagger/cmd/input/list.go index ab752235..1ce2b636 100644 --- a/cmd/dagger/cmd/input/list.go +++ b/cmd/dagger/cmd/input/list.go @@ -30,16 +30,10 @@ var listCmd = &cobra.Command{ lg := logger.New() ctx := lg.WithContext(cmd.Context()) - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - - environment := common.GetCurrentEnvironmentState(ctx, store) + environment := common.GetCurrentEnvironmentState(ctx) lg = lg.With(). - Str("environmentName", environment.Name). - Str("environmentId", environment.ID). + Str("environment", environment.Name). Logger() c, err := dagger.NewClient(ctx, "", false) diff --git a/cmd/dagger/cmd/input/root.go b/cmd/dagger/cmd/input/root.go index d4d1100e..f035fda8 100644 --- a/cmd/dagger/cmd/input/root.go +++ b/cmd/dagger/cmd/input/root.go @@ -6,7 +6,7 @@ import ( "os" "dagger.io/go/cmd/dagger/cmd/common" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -32,21 +32,15 @@ func init() { ) } -func updateEnvironmentInput(ctx context.Context, target string, input dagger.Input) { +func updateEnvironmentInput(ctx context.Context, target string, input state.Input) { lg := log.Ctx(ctx) - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - - st := common.GetCurrentEnvironmentState(ctx, store) + st := common.GetCurrentEnvironmentState(ctx) st.SetInput(target, input) - if err := store.UpdateEnvironment(ctx, st, nil); err != nil { - lg.Fatal().Err(err).Str("environmentId", st.ID).Str("environmentName", st.Name).Msg("cannot update environment") + if err := state.Save(ctx, st); err != nil { + lg.Fatal().Err(err).Str("environment", st.Name).Msg("cannot update environment") } - lg.Info().Str("environmentId", st.ID).Str("environmentName", st.Name).Msg("updated environment") } func readInput(ctx context.Context, source string) string { diff --git a/cmd/dagger/cmd/input/text.go b/cmd/dagger/cmd/input/text.go index 6a2e018e..19f32520 100644 --- a/cmd/dagger/cmd/input/text.go +++ b/cmd/dagger/cmd/input/text.go @@ -2,7 +2,7 @@ package input import ( "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -25,7 +25,7 @@ var textCmd = &cobra.Command{ updateEnvironmentInput( ctx, args[0], - dagger.TextInput(readInput(ctx, args[1])), + state.TextInput(readInput(ctx, args[1])), ) }, } diff --git a/cmd/dagger/cmd/input/unset.go b/cmd/dagger/cmd/input/unset.go index 71a1f78c..248aae7a 100644 --- a/cmd/dagger/cmd/input/unset.go +++ b/cmd/dagger/cmd/input/unset.go @@ -3,7 +3,7 @@ package input import ( "dagger.io/go/cmd/dagger/cmd/common" "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -23,17 +23,12 @@ var unsetCmd = &cobra.Command{ lg := logger.New() ctx := lg.WithContext(cmd.Context()) - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - - st := common.GetCurrentEnvironmentState(ctx, store) + st := common.GetCurrentEnvironmentState(ctx) st.RemoveInputs(args[0]) - if err := store.UpdateEnvironment(ctx, st, nil); err != nil { - lg.Fatal().Err(err).Str("environmentId", st.ID).Str("environmentName", st.Name).Msg("cannot update environment") + if err := state.Save(ctx, st); err != nil { + lg.Fatal().Err(err).Str("environment", st.Name).Msg("cannot update environment") } - lg.Info().Str("environmentId", st.ID).Str("environmentName", st.Name).Msg("updated environment") + lg.Info().Str("environment", st.Name).Msg("updated environment") }, } diff --git a/cmd/dagger/cmd/input/yaml.go b/cmd/dagger/cmd/input/yaml.go index d4216b2f..6afceecd 100644 --- a/cmd/dagger/cmd/input/yaml.go +++ b/cmd/dagger/cmd/input/yaml.go @@ -2,7 +2,7 @@ package input import ( "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -25,7 +25,7 @@ var yamlCmd = &cobra.Command{ updateEnvironmentInput( ctx, args[0], - dagger.YAMLInput(readInput(ctx, args[1])), + state.YAMLInput(readInput(ctx, args[1])), ) }, } diff --git a/cmd/dagger/cmd/list.go b/cmd/dagger/cmd/list.go index 233e879b..fd6b30a1 100644 --- a/cmd/dagger/cmd/list.go +++ b/cmd/dagger/cmd/list.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "fmt" "os" "os/user" @@ -10,7 +11,7 @@ import ( "text/tabwriter" "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -30,12 +31,22 @@ var listCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { lg := logger.New() ctx := lg.WithContext(cmd.Context()) - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") + + 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 := store.ListEnvironments(ctx) + environments, err := state.List(ctx, workspace) if err != nil { lg. Fatal(). @@ -43,45 +54,32 @@ var listCmd = &cobra.Command{ Msg("cannot list environments") } - environmentID := getCurrentEnvironmentID(ctx, store) + environmentPath := getCurrentEnvironmentPath(ctx) w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) - for _, r := range environments { - line := fmt.Sprintf("%s\t%s\t", r.Name, formatPlanSource(r.PlanSource)) - if r.ID == environmentID { + 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) } - w.Flush() }, } -func init() { - if err := viper.BindPFlags(listCmd.Flags()); err != nil { - panic(err) - } -} - -func getCurrentEnvironmentID(ctx context.Context, store *dagger.Store) string { +func getCurrentEnvironmentPath(ctx context.Context) string { lg := log.Ctx(ctx) - wd, err := os.Getwd() + st, err := state.Current(ctx) if err != nil { - lg.Warn().Err(err).Msg("cannot get current working directory") - return "" + // Ignore error if not initialized + if errors.Is(err, state.ErrNotInit) { + return "" + } + lg.Fatal().Err(err).Msg("failed to load current environment") } - st, err := store.LookupEnvironmentByPath(ctx, wd) - if err != nil { - // Ignore error - return "" - } - - if len(st) == 1 { - return st[0].ID - } - - return "" + return st.Path } func formatPath(p string) string { @@ -99,15 +97,8 @@ func formatPath(p string) string { return p } -func formatPlanSource(i dagger.Input) string { - switch i.Type { - case dagger.InputTypeDir: - return formatPath(i.Dir.Path) - case dagger.InputTypeGit: - return i.Git.Remote - case dagger.InputTypeDocker: - return i.Docker.Ref +func init() { + if err := viper.BindPFlags(listCmd.Flags()); err != nil { + panic(err) } - - return "no plan" } diff --git a/cmd/dagger/cmd/new.go b/cmd/dagger/cmd/new.go deleted file mode 100644 index 7d71adae..00000000 --- a/cmd/dagger/cmd/new.go +++ /dev/null @@ -1,148 +0,0 @@ -package cmd - -import ( - "context" - "net/url" - "os" - "path/filepath" - - "dagger.io/go/cmd/dagger/cmd/common" - "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" - - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var newCmd = &cobra.Command{ - Use: "new", - Short: "Create a new environment", - Args: cobra.MaximumNArgs(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()) - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - - if viper.GetString("environment") != "" { - lg. - Fatal(). - Msg("cannot use option -d,--environment for this command") - } - - name := "" - if len(args) > 0 { - name = args[0] - } else { - name = getNewEnvironmentName(ctx) - } - - st := &dagger.EnvironmentState{ - Name: name, - PlanSource: getPlanSource(ctx), - } - - err = store.CreateEnvironment(ctx, st) - if err != nil { - lg.Fatal().Err(err).Msg("failed to create environment") - } - lg. - Info(). - Str("environmentId", st.ID). - Str("environmentName", st.Name). - Msg("environment created") - - if viper.GetBool("up") { - common.EnvironmentUp(ctx, st, false) - } - }, -} - -func getNewEnvironmentName(ctx context.Context) string { - lg := log.Ctx(ctx) - - workDir, err := os.Getwd() - if err != nil { - lg. - Fatal(). - Err(err). - Msg("failed to get current working dir") - } - - currentDir := filepath.Base(workDir) - if currentDir == "/" { - return "root" - } - - return currentDir -} - -func getPlanSource(ctx context.Context) dagger.Input { - lg := log.Ctx(ctx) - - src := dagger.Input{} - checkFirstSet := func() { - if src.Type != dagger.InputTypeEmpty { - lg.Fatal().Msg("only one of those options can be set: --plan-dir, --plan-git, --plan-package, --plan-file") - } - } - - planDir := viper.GetString("plan-dir") - planGit := viper.GetString("plan-git") - - if planDir != "" { - checkFirstSet() - - src = dagger.DirInput(planDir, []string{"*.cue", "cue.mod"}) - } - - if planGit != "" { - checkFirstSet() - - u, err := url.Parse(planGit) - if err != nil { - lg.Fatal().Err(err).Str("url", planGit).Msg("cannot get current working directory") - } - ref := u.Fragment // eg. #main - u.Fragment = "" - remote := u.String() - - src = dagger.GitInput(remote, ref, "") - } - - if src.Type == dagger.InputTypeEmpty { - var err error - wd, err := os.Getwd() - if err != nil { - lg.Fatal().Err(err).Msg("cannot get current working directory") - } - return dagger.DirInput(wd, []string{"*.cue", "cue.mod"}) - } - - return src -} - -func init() { - newCmd.Flags().BoolP("up", "u", false, "Bring the environment online") - - newCmd.Flags().String("plan-dir", "", "Load plan from a local directory") - newCmd.Flags().String("plan-git", "", "Load plan from a git repository") - newCmd.Flags().String("plan-package", "", "Load plan from a cue package") - newCmd.Flags().String("plan-file", "", "Load plan from a cue or json file") - - newCmd.Flags().String("setup", "auto", "Specify whether to prompt user for initial setup (no|yes|auto)") - - if err := viper.BindPFlags(newCmd.Flags()); err != nil { - panic(err) - } -} diff --git a/cmd/dagger/cmd/plan/dir.go b/cmd/dagger/cmd/plan/dir.go deleted file mode 100644 index 10ccf853..00000000 --- a/cmd/dagger/cmd/plan/dir.go +++ /dev/null @@ -1,33 +0,0 @@ -package plan - -import ( - "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var dirCmd = &cobra.Command{ - Use: "dir PATH", - Short: "Load plan from a local directory", - 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()) - - updateEnvironmentPlan(ctx, dagger.DirInput(args[0], []string{"*.cue", "cue.mod"})) - }, -} - -func init() { - if err := viper.BindPFlags(dirCmd.Flags()); err != nil { - panic(err) - } -} diff --git a/cmd/dagger/cmd/plan/file.go b/cmd/dagger/cmd/plan/file.go deleted file mode 100644 index 39f12a1b..00000000 --- a/cmd/dagger/cmd/plan/file.go +++ /dev/null @@ -1,31 +0,0 @@ -package plan - -import ( - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var fileCmd = &cobra.Command{ - Use: "file PATH|-", - Short: "Load plan from a cue file", - 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()) - - panic("not implemented") - }, -} - -func init() { - if err := viper.BindPFlags(fileCmd.Flags()); err != nil { - panic(err) - } -} diff --git a/cmd/dagger/cmd/plan/git.go b/cmd/dagger/cmd/plan/git.go deleted file mode 100644 index 35153ba3..00000000 --- a/cmd/dagger/cmd/plan/git.go +++ /dev/null @@ -1,43 +0,0 @@ -package plan - -import ( - "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var gitCmd = &cobra.Command{ - Use: "git REMOTE [REF] [SUBDIR]", - Short: "Load plan from a git package", - Args: cobra.RangeArgs(1, 3), - 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()) - - ref := "HEAD" - if len(args) > 1 { - ref = args[1] - } - - subDir := "" - if len(args) > 2 { - subDir = args[2] - } - - updateEnvironmentPlan(ctx, dagger.GitInput(args[0], ref, subDir)) - }, -} - -func init() { - if err := viper.BindPFlags(gitCmd.Flags()); err != nil { - panic(err) - } -} diff --git a/cmd/dagger/cmd/plan/package.go b/cmd/dagger/cmd/plan/package.go deleted file mode 100644 index 215225cb..00000000 --- a/cmd/dagger/cmd/plan/package.go +++ /dev/null @@ -1,31 +0,0 @@ -package plan - -import ( - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var packageCmd = &cobra.Command{ - Use: "package PKG", - Short: "Load plan from a cue package", - 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()) - - panic("not implemented") - }, -} - -func init() { - if err := viper.BindPFlags(packageCmd.Flags()); err != nil { - panic(err) - } -} diff --git a/cmd/dagger/cmd/plan/root.go b/cmd/dagger/cmd/plan/root.go deleted file mode 100644 index 0bf82327..00000000 --- a/cmd/dagger/cmd/plan/root.go +++ /dev/null @@ -1,42 +0,0 @@ -package plan - -import ( - "context" - - "dagger.io/go/cmd/dagger/cmd/common" - "dagger.io/go/dagger" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -// Cmd exposes the top-level command -var Cmd = &cobra.Command{ - Use: "plan", - Short: "Manage an environment's plan", -} - -func init() { - Cmd.AddCommand( - packageCmd, - dirCmd, - gitCmd, - fileCmd, - ) -} - -func updateEnvironmentPlan(ctx context.Context, planSource dagger.Input) { - lg := log.Ctx(ctx) - - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - - st := common.GetCurrentEnvironmentState(ctx, store) - st.PlanSource = planSource - - if err := store.UpdateEnvironment(ctx, st, nil); err != nil { - lg.Fatal().Err(err).Str("environmentId", st.ID).Str("environmentName", st.Name).Msg("cannot update environment") - } - lg.Info().Str("environmentId", st.ID).Str("environmentName", st.Name).Msg("updated environment") -} diff --git a/cmd/dagger/cmd/query.go b/cmd/dagger/cmd/query.go index f64933b3..62981608 100644 --- a/cmd/dagger/cmd/query.go +++ b/cmd/dagger/cmd/query.go @@ -30,16 +30,10 @@ var queryCmd = &cobra.Command{ cueOpts := parseQueryFlags() - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - - state := common.GetCurrentEnvironmentState(ctx, store) + state := common.GetCurrentEnvironmentState(ctx) lg = lg.With(). - Str("environmentName", state.Name). - Str("environmentId", state.ID). + Str("environment", state.Name). Logger() cuePath := cue.MakePath() diff --git a/cmd/dagger/cmd/root.go b/cmd/dagger/cmd/root.go index 2c5da999..09868d79 100644 --- a/cmd/dagger/cmd/root.go +++ b/cmd/dagger/cmd/root.go @@ -6,7 +6,6 @@ import ( "dagger.io/go/cmd/dagger/cmd/input" "dagger.io/go/cmd/dagger/cmd/output" - "dagger.io/go/cmd/dagger/cmd/plan" "dagger.io/go/cmd/dagger/logger" "github.com/moby/buildkit/util/appcontext" "github.com/opentracing/opentracing-go" @@ -24,6 +23,7 @@ func init() { rootCmd.PersistentFlags().String("log-format", "", "Log format (json, pretty). Defaults to json if the terminal is not a tty") rootCmd.PersistentFlags().StringP("log-level", "l", "info", "Log level") rootCmd.PersistentFlags().StringP("environment", "e", "", "Select an environment") + rootCmd.PersistentFlags().StringP("workspace", "w", "", "Specify a workspace (defaults to current git repository)") rootCmd.PersistentPreRun = func(*cobra.Command, []string) { go checkVersion() @@ -33,8 +33,8 @@ func init() { } rootCmd.AddCommand( + initCmd, computeCmd, - newCmd, listCmd, queryCmd, upCmd, @@ -43,7 +43,6 @@ func init() { historyCmd, loginCmd, logoutCmd, - plan.Cmd, input.Cmd, output.Cmd, versionCmd, diff --git a/cmd/dagger/cmd/up.go b/cmd/dagger/cmd/up.go index 2daed27e..808313e6 100644 --- a/cmd/dagger/cmd/up.go +++ b/cmd/dagger/cmd/up.go @@ -3,7 +3,7 @@ package cmd import ( "dagger.io/go/cmd/dagger/cmd/common" "dagger.io/go/cmd/dagger/logger" - "dagger.io/go/dagger" + "dagger.io/go/dagger/state" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -23,15 +23,12 @@ var upCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { lg := logger.New() ctx := lg.WithContext(cmd.Context()) - store, err := dagger.DefaultStore() - if err != nil { - lg.Fatal().Err(err).Msg("failed to load store") - } - state := common.GetCurrentEnvironmentState(ctx, store) - result := common.EnvironmentUp(ctx, state, viper.GetBool("no-cache")) - state.Computed = result.Computed().JSON().String() - if err := store.UpdateEnvironment(ctx, state, nil); err != nil { + st := common.GetCurrentEnvironmentState(ctx) + result := common.EnvironmentUp(ctx, st, viper.GetBool("no-cache")) + + st.Computed = result.Computed().JSON().PrettyString() + if err := state.Save(ctx, st); err != nil { lg.Fatal().Err(err).Msg("failed to update environment") } }, diff --git a/dagger/client.go b/dagger/client.go index 1ea41978..3fcd8ff3 100644 --- a/dagger/client.go +++ b/dagger/client.go @@ -26,6 +26,7 @@ import ( "dagger.io/go/pkg/progressui" "dagger.io/go/dagger/compiler" + "dagger.io/go/dagger/state" ) // A dagger client @@ -63,7 +64,7 @@ func NewClient(ctx context.Context, host string, noCache bool) (*Client, error) type ClientDoFunc func(context.Context, *Environment, Solver) error // FIXME: return completed *Route, instead of *compiler.Value -func (c *Client) Do(ctx context.Context, state *EnvironmentState, fn ClientDoFunc) (*Environment, error) { +func (c *Client) Do(ctx context.Context, state *state.State, fn ClientDoFunc) (*Environment, error) { lg := log.Ctx(ctx) eg, gctx := errgroup.WithContext(ctx) diff --git a/dagger/compiler/json.go b/dagger/compiler/json.go index e138f7af..4f67da92 100644 --- a/dagger/compiler/json.go +++ b/dagger/compiler/json.go @@ -112,5 +112,5 @@ func (s JSON) PrettyString() string { if err := json.Indent(b, []byte(raw), "", " "); err != nil { return raw } - return b.String() + return fmt.Sprintf("%s\n", b.String()) } diff --git a/dagger/environment.go b/dagger/environment.go index c0177676..b4048cd4 100644 --- a/dagger/environment.go +++ b/dagger/environment.go @@ -10,6 +10,8 @@ import ( "cuelang.org/go/cue" cueflow "cuelang.org/go/tools/flow" "dagger.io/go/dagger/compiler" + "dagger.io/go/dagger/state" + "dagger.io/go/pkg/cuetils" "dagger.io/go/stdlib" "github.com/opentracing/opentracing-go" @@ -19,7 +21,7 @@ import ( ) type Environment struct { - state *EnvironmentState + state *state.State // Layer 1: plan configuration plan *compiler.Value @@ -31,7 +33,7 @@ type Environment struct { computed *compiler.Value } -func NewEnvironment(st *EnvironmentState) (*Environment, error) { +func NewEnvironment(st *state.State) (*Environment, error) { e := &Environment{ state: st, @@ -42,7 +44,7 @@ func NewEnvironment(st *EnvironmentState) (*Environment, error) { // Prepare inputs for _, input := range st.Inputs { - v, err := input.Value.Compile() + v, err := input.Value.Compile(st) if err != nil { return nil, err } @@ -59,16 +61,12 @@ func NewEnvironment(st *EnvironmentState) (*Environment, error) { return e, nil } -func (e *Environment) ID() string { - return e.state.ID -} - func (e *Environment) Name() string { return e.state.Name } -func (e *Environment) PlanSource() Input { - return e.state.PlanSource +func (e *Environment) PlanSource() state.Input { + return e.state.PlanSource() } func (e *Environment) Plan() *compiler.Value { @@ -88,7 +86,7 @@ func (e *Environment) LoadPlan(ctx context.Context, s Solver) error { span, ctx := opentracing.StartSpanFromContext(ctx, "environment.LoadPlan") defer span.Finish() - planSource, err := e.state.PlanSource.Compile() + planSource, err := e.state.PlanSource().Compile(e.state) if err != nil { return err } @@ -159,7 +157,7 @@ func (e *Environment) LocalDirs() map[string]string { } // 2. Scan the plan - plan, err := e.state.PlanSource.Compile() + plan, err := e.state.PlanSource().Compile(e.state) if err != nil { panic(err) } diff --git a/dagger/input_test.go b/dagger/environment_test.go similarity index 61% rename from dagger/input_test.go rename to dagger/environment_test.go index 8fe60262..cb200b8d 100644 --- a/dagger/input_test.go +++ b/dagger/environment_test.go @@ -3,14 +3,15 @@ package dagger import ( "testing" + "dagger.io/go/dagger/state" "github.com/stretchr/testify/require" ) -func TestInputDir(t *testing.T) { - st := &EnvironmentState{ - PlanSource: DirInput("/tmp/source", []string{}), +func TestLocalDirs(t *testing.T) { + st := &state.State{ + Path: "/tmp/source", } - require.NoError(t, st.SetInput("www.source", DirInput("/", []string{}))) + require.NoError(t, st.SetInput("www.source", state.DirInput("/", []string{}))) environment, err := NewEnvironment(st) require.NoError(t, err) diff --git a/dagger/input.go b/dagger/state/input.go similarity index 77% rename from dagger/input.go rename to dagger/state/input.go index 563cb017..c1ad5676 100644 --- a/dagger/input.go +++ b/dagger/state/input.go @@ -1,9 +1,10 @@ -package dagger +package state import ( "encoding/json" "fmt" "io/ioutil" + "path" "path/filepath" "cuelang.org/go/cue" @@ -37,33 +38,33 @@ const ( ) type Input struct { - Type InputType `json:"type,omitempty"` + Type InputType `yaml:"type,omitempty"` - Dir *dirInput `json:"dir,omitempty"` - Git *gitInput `json:"git,omitempty"` - Docker *dockerInput `json:"docker,omitempty"` - Text *textInput `json:"text,omitempty"` - JSON *jsonInput `json:"json,omitempty"` - YAML *yamlInput `json:"yaml,omitempty"` - File *fileInput `json:"file,omitempty"` + Dir *dirInput `yaml:"dir,omitempty"` + Git *gitInput `yaml:"git,omitempty"` + Docker *dockerInput `yaml:"docker,omitempty"` + Text *textInput `yaml:"text,omitempty"` + JSON *jsonInput `yaml:"json,omitempty"` + YAML *yamlInput `yaml:"yaml,omitempty"` + File *fileInput `yaml:"file,omitempty"` } -func (i Input) Compile() (*compiler.Value, error) { +func (i Input) Compile(state *State) (*compiler.Value, error) { switch i.Type { case InputTypeDir: - return i.Dir.Compile() + return i.Dir.Compile(state) case InputTypeGit: - return i.Git.Compile() + return i.Git.Compile(state) case InputTypeDocker: - return i.Docker.Compile() + return i.Docker.Compile(state) case InputTypeText: - return i.Text.Compile() + return i.Text.Compile(state) case InputTypeJSON: - return i.JSON.Compile() + return i.JSON.Compile(state) case InputTypeYAML: - return i.YAML.Compile() + return i.YAML.Compile(state) case InputTypeFile: - return i.File.Compile() + return i.File.Compile(state) case "": return nil, fmt.Errorf("input has not been set") default: @@ -73,12 +74,6 @@ func (i Input) Compile() (*compiler.Value, error) { // An input artifact loaded from a local directory func DirInput(path string, include []string) Input { - // resolve absolute path - path, err := filepath.Abs(path) - if err != nil { - panic(err) - } - return Input{ Type: InputTypeDir, Dir: &dirInput{ @@ -93,7 +88,7 @@ type dirInput struct { Include []string `json:"include,omitempty"` } -func (dir dirInput) Compile() (*compiler.Value, error) { +func (dir dirInput) Compile(state *State) (*compiler.Value, error) { // FIXME: serialize an intermediate struct, instead of generating cue source // json.Marshal([]string{}) returns []byte("null"), which wreaks havoc @@ -106,9 +101,15 @@ func (dir dirInput) Compile() (*compiler.Value, error) { return nil, err } } + + p := dir.Path + if !filepath.IsAbs(p) { + p = filepath.Clean(path.Join(state.Path, p)) + } + llb := fmt.Sprintf( `#up: [{do:"local",dir:"%s", include:%s}]`, - dir.Path, + p, includeLLB, ) return compiler.Compile("", llb) @@ -132,7 +133,7 @@ func GitInput(remote, ref, dir string) Input { } } -func (git gitInput) Compile() (*compiler.Value, error) { +func (git gitInput) Compile(_ *State) (*compiler.Value, error) { ref := "HEAD" if git.Ref != "" { ref = git.Ref @@ -159,7 +160,7 @@ type dockerInput struct { Ref string `json:"ref,omitempty"` } -func (i dockerInput) Compile() (*compiler.Value, error) { +func (i dockerInput) Compile(_ *State) (*compiler.Value, error) { panic("NOT IMPLEMENTED") } @@ -177,7 +178,7 @@ type textInput struct { Data string `json:"data,omitempty"` } -func (i textInput) Compile() (*compiler.Value, error) { +func (i textInput) Compile(_ *State) (*compiler.Value, error) { return compiler.Compile("", fmt.Sprintf("%q", i.Data)) } @@ -196,7 +197,7 @@ type jsonInput struct { Data string `json:"data,omitempty"` } -func (i jsonInput) Compile() (*compiler.Value, error) { +func (i jsonInput) Compile(_ *State) (*compiler.Value, error) { return compiler.DecodeJSON("", []byte(i.Data)) } @@ -215,7 +216,7 @@ type yamlInput struct { Data string `json:"data,omitempty"` } -func (i yamlInput) Compile() (*compiler.Value, error) { +func (i yamlInput) Compile(_ *State) (*compiler.Value, error) { return compiler.DecodeYAML("", []byte(i.Data)) } @@ -232,7 +233,7 @@ type fileInput struct { Path string `json:"data,omitempty"` } -func (i fileInput) Compile() (*compiler.Value, error) { +func (i fileInput) Compile(_ *State) (*compiler.Value, error) { data, err := ioutil.ReadFile(i.Path) if err != nil { return nil, err diff --git a/dagger/state.go b/dagger/state/state.go similarity index 58% rename from dagger/state.go rename to dagger/state/state.go index 6276eaa9..619824c5 100644 --- a/dagger/state.go +++ b/dagger/state/state.go @@ -1,32 +1,34 @@ -package dagger +package state // Contents of an environment serialized to a file -type EnvironmentState struct { - // Globally unique environment ID - ID string `json:"id,omitempty"` +type State struct { + // State path + Path string `yaml:"-"` // Human-friendly environment name. // A environment may have more than one name. // FIXME: store multiple names? - Name string `json:"name,omitempty"` - - // Cue module containing the environment plan - // The input's top-level artifact is used as a module directory. - PlanSource Input `json:"plan,omitempty"` + Name string `yaml:"name,omitempty"` // User Inputs - Inputs []inputKV `json:"inputs,omitempty"` + Inputs []inputKV `yaml:"inputs,omitempty"` // Computed values - Computed string `json:"output,omitempty"` + Computed string `yaml:"-"` } type inputKV struct { - Key string `json:"key,omitempty"` - Value Input `json:"value,omitempty"` + Key string `yaml:"key,omitempty"` + Value Input `yaml:"value,omitempty"` } -func (s *EnvironmentState) SetInput(key string, value Input) error { +// 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"}) +} + +func (s *State) SetInput(key string, value Input) error { for i, inp := range s.Inputs { if inp.Key != key { continue @@ -42,7 +44,7 @@ func (s *EnvironmentState) SetInput(key string, value Input) error { // Remove all inputs at the given key, including sub-keys. // For example RemoveInputs("foo.bar") will remove all inputs // at foo.bar, foo.bar.baz, etc. -func (s *EnvironmentState) RemoveInputs(key string) error { +func (s *State) RemoveInputs(key string) error { newInputs := make([]inputKV, 0, len(s.Inputs)) for _, i := range s.Inputs { if i.Key == key { diff --git a/dagger/state/store.go b/dagger/state/store.go new file mode 100644 index 00000000..966d4494 --- /dev/null +++ b/dagger/state/store.go @@ -0,0 +1,205 @@ +package state + +import ( + "context" + "errors" + "os" + "path" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +var ( + ErrNotInit = errors.New("not initialized") + ErrAlreadyInit = errors.New("already initialized") + ErrNoCurrentWorkspace = errors.New("not in a git directory") +) + +const ( + daggerDir = ".dagger" + stateDir = "state" + manifestFile = "values.yaml" + computedFile = "computed.json" +) + +func Init(ctx context.Context, dir, name string) (*State, error) { + root := path.Join(dir, daggerDir) + err := os.Mkdir(root, 0755) + if err != nil { + if errors.Is(err, os.ErrExist) { + return nil, ErrAlreadyInit + } + return nil, err + } + + err = os.WriteFile( + path.Join(root, ".gitignore"), + []byte("# dagger state\nstate/**\n"), + 0600, + ) + if err != nil { + return nil, err + } + + st := &State{ + Path: dir, + Name: name, + } + + return st, Save(ctx, st) +} + +func Current(ctx context.Context) (*State, error) { + current, err := os.Getwd() + if err != nil { + return nil, err + } + + // Walk every parent directory to find .dagger + for { + _, err := os.Stat(path.Join(current, daggerDir)) + if err == nil { + return Open(ctx, current) + } + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } + + return nil, ErrNotInit +} + +func Open(ctx context.Context, dir string) (*State, 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 + } + + data, err := os.ReadFile(path.Join(root, daggerDir, manifestFile)) + if err != nil { + return nil, err + } + var st State + if err := yaml.Unmarshal(data, &st); err != nil { + return nil, err + } + st.Path = root + + computed, err := os.ReadFile(path.Join(root, daggerDir, stateDir, computedFile)) + if err == nil { + st.Computed = string(computed) + } + + return &st, nil +} + +func Save(ctx context.Context, st *State) error { + data, err := yaml.Marshal(st) + if err != nil { + return err + } + + if err := os.WriteFile(path.Join(st.Path, daggerDir, manifestFile), data, 0600); err != nil { + return err + } + + if st.Computed != "" { + state := path.Join(st.Path, daggerDir, stateDir) + if err := os.MkdirAll(state, 0755); err != nil { + return err + } + err := os.WriteFile( + path.Join(state, "computed.json"), + []byte(st.Computed), + 0600) + if err != nil { + return err + } + } + + return nil +} + +func CurrentWorkspace(ctx context.Context) (string, error) { + current, err := os.Getwd() + if err != nil { + return "", err + } + + // Walk every parent directory to find .dagger + for { + _, err := os.Stat(path.Join(current, ".git")) + if err == nil { + return current, nil + } + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } + + return "", ErrNoCurrentWorkspace +} + +func List(ctx context.Context, workspace string) ([]*State, error) { + var ( + environments = []*State{} + err error + ) + + 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 +} diff --git a/dagger/state/store_test.go b/dagger/state/store_test.go new file mode 100644 index 00000000..d5ec0afd --- /dev/null +++ b/dagger/state/store_test.go @@ -0,0 +1,39 @@ +package state + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStore(t *testing.T) { + ctx := context.TODO() + + // Init + root, err := os.MkdirTemp(os.TempDir(), "dagger-*") + require.NoError(t, err) + st, err := Init(ctx, root, "test") + require.Equal(t, "test", st.Name) + require.Equal(t, root, st.Path) + require.NoError(t, err) + + // Open + _, err = Open(ctx, "/tmp/not/exist") + require.Error(t, err) + require.ErrorIs(t, ErrNotInit, err) + + st, err = Open(ctx, root) + require.NoError(t, err) + require.Equal(t, "test", st.Name) + require.Equal(t, root, st.Path) + + // Save + computed := `{"hello": "world"}` + st.Computed = computed + require.NoError(t, Save(ctx, st)) + st, err = Open(ctx, root) + require.NoError(t, err) + require.Equal(t, computed, st.Computed) +} diff --git a/dagger/store.go b/dagger/store.go deleted file mode 100644 index d203cb70..00000000 --- a/dagger/store.go +++ /dev/null @@ -1,250 +0,0 @@ -package dagger - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path" - "sync" - - "github.com/google/uuid" -) - -var ( - ErrEnvironmentExist = errors.New("environment already exists") - ErrEnvironmentNotExist = errors.New("environment doesn't exist") -) - -const ( - defaultStoreRoot = "$HOME/.dagger/store" -) - -type Store struct { - root string - - l sync.RWMutex - - // ID -> Environment - environments map[string]*EnvironmentState - - // Name -> Environment - environmentsByName map[string]*EnvironmentState - - // Path -> (ID->Environment) - environmentsByPath map[string]map[string]*EnvironmentState - - // ID -> (Path->{}) - pathsByEnvironmentID map[string]map[string]struct{} -} - -func NewStore(root string) (*Store, error) { - store := &Store{ - root: root, - environments: make(map[string]*EnvironmentState), - environmentsByName: make(map[string]*EnvironmentState), - environmentsByPath: make(map[string]map[string]*EnvironmentState), - pathsByEnvironmentID: make(map[string]map[string]struct{}), - } - return store, store.loadAll() -} - -func DefaultStore() (*Store, error) { - if root := os.Getenv("DAGGER_STORE"); root != "" { - return NewStore(root) - } - - return NewStore(os.ExpandEnv(defaultStoreRoot)) -} - -func (s *Store) environmentPath(name string) string { - // FIXME: rename to environment.json ? - return path.Join(s.root, name, "deployment.json") -} - -func (s *Store) loadAll() error { - files, err := os.ReadDir(s.root) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return err - } - - for _, f := range files { - if !f.IsDir() { - continue - } - if err := s.loadEnvironment(f.Name()); err != nil { - return err - } - } - - return nil -} - -func (s *Store) loadEnvironment(name string) error { - data, err := os.ReadFile(s.environmentPath(name)) - if err != nil { - return err - } - var st EnvironmentState - if err := json.Unmarshal(data, &st); err != nil { - return err - } - s.indexEnvironment(&st) - return nil -} - -func (s *Store) syncEnvironment(r *EnvironmentState) error { - p := s.environmentPath(r.Name) - - if err := os.MkdirAll(path.Dir(p), 0755); err != nil { - return err - } - - data, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err - } - - if err := os.WriteFile(p, data, 0600); err != nil { - return err - } - - s.reindexEnvironment(r) - - return nil -} - -func (s *Store) indexEnvironment(r *EnvironmentState) { - s.environments[r.ID] = r - s.environmentsByName[r.Name] = r - - mapPath := func(i Input) { - if i.Type != InputTypeDir { - return - } - if s.environmentsByPath[i.Dir.Path] == nil { - s.environmentsByPath[i.Dir.Path] = make(map[string]*EnvironmentState) - } - s.environmentsByPath[i.Dir.Path][r.ID] = r - - if s.pathsByEnvironmentID[r.ID] == nil { - s.pathsByEnvironmentID[r.ID] = make(map[string]struct{}) - } - s.pathsByEnvironmentID[r.ID][i.Dir.Path] = struct{}{} - } - - mapPath(r.PlanSource) - for _, i := range r.Inputs { - mapPath(i.Value) - } -} - -func (s *Store) deindexEnvironment(id string) { - r, ok := s.environments[id] - if !ok { - return - } - delete(s.environments, r.ID) - delete(s.environmentsByName, r.Name) - - for p := range s.pathsByEnvironmentID[r.ID] { - delete(s.environmentsByPath[p], r.ID) - } - delete(s.pathsByEnvironmentID, r.ID) -} - -func (s *Store) reindexEnvironment(r *EnvironmentState) { - s.deindexEnvironment(r.ID) - s.indexEnvironment(r) -} - -func (s *Store) CreateEnvironment(ctx context.Context, st *EnvironmentState) error { - s.l.Lock() - defer s.l.Unlock() - - if _, ok := s.environmentsByName[st.Name]; ok { - return fmt.Errorf("%s: %w", st.Name, ErrEnvironmentExist) - } - - st.ID = uuid.New().String() - return s.syncEnvironment(st) -} - -type UpdateOpts struct{} - -func (s *Store) UpdateEnvironment(ctx context.Context, r *EnvironmentState, o *UpdateOpts) error { - s.l.Lock() - defer s.l.Unlock() - - return s.syncEnvironment(r) -} - -type DeleteOpts struct{} - -func (s *Store) DeleteEnvironment(ctx context.Context, r *EnvironmentState, o *DeleteOpts) error { - s.l.Lock() - defer s.l.Unlock() - - if err := os.Remove(s.environmentPath(r.Name)); err != nil { - return err - } - s.deindexEnvironment(r.ID) - return nil -} - -func (s *Store) LookupEnvironmentByID(ctx context.Context, id string) (*EnvironmentState, error) { - s.l.RLock() - defer s.l.RUnlock() - - st, ok := s.environments[id] - if !ok { - return nil, fmt.Errorf("%s: %w", id, ErrEnvironmentNotExist) - } - return st, nil -} - -func (s *Store) LookupEnvironmentByName(ctx context.Context, name string) (*EnvironmentState, error) { - s.l.RLock() - defer s.l.RUnlock() - - st, ok := s.environmentsByName[name] - if !ok { - return nil, fmt.Errorf("%s: %w", name, ErrEnvironmentNotExist) - } - return st, nil -} - -func (s *Store) LookupEnvironmentByPath(ctx context.Context, path string) ([]*EnvironmentState, error) { - s.l.RLock() - defer s.l.RUnlock() - - res := []*EnvironmentState{} - - environments, ok := s.environmentsByPath[path] - if !ok { - return res, nil - } - - for _, d := range environments { - res = append(res, d) - } - - return res, nil -} - -func (s *Store) ListEnvironments(ctx context.Context) ([]*EnvironmentState, error) { - s.l.RLock() - defer s.l.RUnlock() - - environments := make([]*EnvironmentState, 0, len(s.environments)) - - for _, st := range s.environments { - environments = append(environments, st) - } - - return environments, nil -} diff --git a/dagger/store_test.go b/dagger/store_test.go deleted file mode 100644 index 2f9d6332..00000000 --- a/dagger/store_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package dagger - -import ( - "context" - "errors" - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestStoreLoad(t *testing.T) { - ctx := context.TODO() - - root, err := os.MkdirTemp(os.TempDir(), "dagger-*") - require.NoError(t, err) - store, err := NewStore(root) - require.NoError(t, err) - - _, err = store.LookupEnvironmentByName(ctx, "notexist") - require.Error(t, err) - require.True(t, errors.Is(err, ErrEnvironmentNotExist)) - - st := &EnvironmentState{ - Name: "test", - } - require.NoError(t, store.CreateEnvironment(ctx, st)) - - checkEnvironments := func(store *Store) { - r, err := store.LookupEnvironmentByID(ctx, st.ID) - require.NoError(t, err) - require.NotNil(t, r) - require.Equal(t, "test", r.Name) - - r, err = store.LookupEnvironmentByName(ctx, "test") - require.NoError(t, err) - require.NotNil(t, r) - require.Equal(t, "test", r.Name) - - environments, err := store.ListEnvironments(ctx) - require.NoError(t, err) - require.Len(t, environments, 1) - require.Equal(t, "test", environments[0].Name) - } - - checkEnvironments(store) - - // Reload the environments from disk and check again - newStore, err := NewStore(root) - require.NoError(t, err) - checkEnvironments(newStore) -} - -func TestStoreLookupByPath(t *testing.T) { - ctx := context.TODO() - - root, err := os.MkdirTemp(os.TempDir(), "dagger-*") - require.NoError(t, err) - store, err := NewStore(root) - require.NoError(t, err) - - st := &EnvironmentState{ - Name: "test", - } - require.NoError(t, st.SetInput("foo", DirInput("/test/path", []string{}))) - require.NoError(t, store.CreateEnvironment(ctx, st)) - - // Lookup by path - environments, err := store.LookupEnvironmentByPath(ctx, "/test/path") - require.NoError(t, err) - require.Len(t, environments, 1) - require.Equal(t, st.ID, environments[0].ID) - - // Add a new path - require.NoError(t, st.SetInput("bar", DirInput("/test/anotherpath", []string{}))) - require.NoError(t, store.UpdateEnvironment(ctx, st, nil)) - - // Lookup by the previous path - environments, err = store.LookupEnvironmentByPath(ctx, "/test/path") - require.NoError(t, err) - require.Len(t, environments, 1) - require.Equal(t, st.ID, environments[0].ID) - - // Lookup by the new path - environments, err = store.LookupEnvironmentByPath(ctx, "/test/anotherpath") - require.NoError(t, err) - require.Len(t, environments, 1) - require.Equal(t, st.ID, environments[0].ID) - - // Remove a path - require.NoError(t, st.RemoveInputs("foo")) - require.NoError(t, store.UpdateEnvironment(ctx, st, nil)) - - // Lookup by the removed path should fail - environments, err = store.LookupEnvironmentByPath(ctx, "/test/path") - require.NoError(t, err) - require.Len(t, environments, 0) - - // Lookup by the other path should still work - environments, err = store.LookupEnvironmentByPath(ctx, "/test/anotherpath") - require.NoError(t, err) - require.Len(t, environments, 1) - - // Add another environment using the same path - otherSt := &EnvironmentState{ - Name: "test2", - } - require.NoError(t, otherSt.SetInput("foo", DirInput("/test/anotherpath", []string{}))) - require.NoError(t, store.CreateEnvironment(ctx, otherSt)) - - // Lookup by path should return both environments - environments, err = store.LookupEnvironmentByPath(ctx, "/test/anotherpath") - require.NoError(t, err) - require.Len(t, environments, 2) - - // Remove the first environment. Lookup by path should still return the - // second environment. - require.NoError(t, store.DeleteEnvironment(ctx, st, nil)) - environments, err = store.LookupEnvironmentByPath(ctx, "/test/anotherpath") - require.NoError(t, err) - require.Len(t, environments, 1) - require.Equal(t, otherSt.ID, environments[0].ID) -} diff --git a/go.mod b/go.mod index 5cfc1763..3029f551 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/containerd/console v1.0.2 github.com/docker/distribution v2.7.1+incompatible github.com/emicklei/proto v1.9.0 // indirect - github.com/google/uuid v1.2.0 github.com/hashicorp/go-version v1.3.0 github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea github.com/mattn/go-colorable v0.1.8 // indirect