diff --git a/cmd/dagger/cmd/common/common.go b/cmd/dagger/cmd/common/common.go new file mode 100644 index 00000000..c7f3f527 --- /dev/null +++ b/cmd/dagger/cmd/common/common.go @@ -0,0 +1,73 @@ +package common + +import ( + "context" + "fmt" + "os" + + "dagger.io/go/dagger" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// getCurrentRoute returns the current selected route based on its abs path +func GetCurrentRoute(ctx context.Context, store *dagger.Store) *dagger.Route { + lg := log.Ctx(ctx) + st := GetCurrentRouteState(ctx, store) + + route, err := dagger.NewRoute(st) + if err != nil { + lg. + Fatal(). + Err(err). + Interface("routeState", st). + Msg("failed to init route") + } + + return route +} + +func GetCurrentRouteState(ctx context.Context, store *dagger.Store) *dagger.RouteState { + lg := log.Ctx(ctx) + + routeName := viper.GetString("route") + if routeName != "" { + st, err := store.LookupRouteByName(ctx, routeName) + if err != nil { + lg. + Fatal(). + Err(err). + Str("routeName", routeName). + Msg("failed to lookup route by name") + } + return st + } + + wd, err := os.Getwd() + if err != nil { + lg.Fatal().Err(err).Msg("cannot get current working directory") + } + st, err := store.LookupRouteByPath(ctx, wd) + if err != nil { + lg. + Fatal(). + Err(err). + Str("routePath", wd). + Msg("failed to lookup route by path") + } + return st +} + +func RouteUp(ctx context.Context, route *dagger.Route) { + lg := log.Ctx(ctx) + + c, err := dagger.NewClient(ctx, "") + if err != nil { + lg.Fatal().Err(err).Msg("unable to create client") + } + output, err := c.Up(ctx, route) + if err != nil { + lg.Fatal().Err(err).Msg("failed to compute") + } + fmt.Println(output.JSON()) +} diff --git a/cmd/dagger/cmd/compute.go b/cmd/dagger/cmd/compute.go index dcee0ffb..86054007 100644 --- a/cmd/dagger/cmd/compute.go +++ b/cmd/dagger/cmd/compute.go @@ -1,20 +1,22 @@ package cmd import ( - "fmt" + "encoding/json" + "errors" + "os" + "strings" + "dagger.io/go/cmd/dagger/cmd/common" "dagger.io/go/cmd/dagger/logger" "dagger.io/go/dagger" + "go.mozilla.org/sops" + "go.mozilla.org/sops/decrypt" + "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var ( - input *dagger.InputValue - updater *dagger.InputValue -) - var computeCmd = &cobra.Command{ Use: "compute CONFIG", Short: "Compute a configuration", @@ -30,51 +32,116 @@ var computeCmd = &cobra.Command{ lg := logger.New() ctx := lg.WithContext(cmd.Context()) - env, err := dagger.NewEnv() - if err != nil { - lg.Fatal().Err(err).Msg("unable to initialize environment") - } - if err := updater.SourceFlag().Set(args[0]); err != nil { - lg.Fatal().Err(err).Msg("invalid local source") + st := &dagger.RouteState{ + ID: uuid.New().String(), + Name: "FIXME", + LayoutSource: dagger.DirInput(args[0], []string{"*.cue", "cue.mod"}), } - if err := env.SetUpdater(updater.Value()); err != nil { - lg.Fatal().Err(err).Msg("invalid updater script") + for _, input := range viper.GetStringSlice("input-string") { + parts := strings.SplitN(input, "=", 2) + k, v := parts[0], parts[1] + err := st.AddInput(k, dagger.TextInput(v)) + if err != nil { + lg. + Fatal(). + Err(err). + Str("input", k). + Msg("failed to add input") + } } - if err := env.SetInput(input.Value()); err != nil { - lg.Fatal().Err(err).Msg("invalid input") + + for _, input := range viper.GetStringSlice("input-dir") { + parts := strings.SplitN(input, "=", 2) + k, v := parts[0], parts[1] + err := st.AddInput(k, dagger.DirInput(v, []string{})) + if err != nil { + lg. + Fatal(). + Err(err). + Str("input", k). + Msg("failed to add input") + } } - c, err := dagger.NewClient(ctx, "") + + for _, input := range viper.GetStringSlice("input-git") { + parts := strings.SplitN(input, "=", 2) + k, v := parts[0], parts[1] + err := st.AddInput(k, dagger.GitInput(v, "", "")) + if err != nil { + lg. + Fatal(). + Err(err). + Str("input", k). + Msg("failed to add input") + } + } + + if f := viper.GetString("input-json"); f != "" { + lg := lg.With().Str("path", f).Logger() + + content, err := os.ReadFile(f) + if err != nil { + lg.Fatal().Err(err).Msg("failed to read file") + } + + plaintext, err := decrypt.Data(content, "json") + if err != nil && !errors.Is(err, sops.MetadataNotFound) { + lg.Fatal().Err(err).Msg("unable to decrypt") + } + + if len(plaintext) > 0 { + content = plaintext + } + + if !json.Valid(content) { + lg.Fatal().Msg("invalid json") + } + + err = st.AddInput("", dagger.JSONInput(string(content))) + if err != nil { + lg.Fatal().Err(err).Msg("failed to add input") + } + } + + if f := viper.GetString("input-yaml"); f != "" { + lg := lg.With().Str("path", f).Logger() + + content, err := os.ReadFile(f) + if err != nil { + lg.Fatal().Err(err).Msg("failed to read file") + } + + plaintext, err := decrypt.Data(content, "yaml") + if err != nil && !errors.Is(err, sops.MetadataNotFound) { + lg.Fatal().Err(err).Msg("unable to decrypt") + } + + if len(plaintext) > 0 { + content = plaintext + } + + err = st.AddInput("", dagger.YAMLInput(string(content))) + if err != nil { + lg.Fatal().Err(err).Msg("failed to add input") + } + } + + route, err := dagger.NewRoute(st) if err != nil { - lg.Fatal().Err(err).Msg("unable to create client") + lg.Fatal().Err(err).Msg("unable to initialize route") } - output, err := c.Compute(ctx, env) - if err != nil { - lg.Fatal().Err(err).Msg("failed to compute") - } - fmt.Println(output.JSON()) + + common.RouteUp(ctx, route) }, } func init() { - var err error - // Setup --input-* flags - input, err = dagger.NewInputValue("{}") - if err != nil { - panic(err) - } - computeCmd.Flags().Var(input.StringFlag(), "input-string", "TARGET=STRING") - computeCmd.Flags().Var(input.DirFlag(), "input-dir", "TARGET=PATH") - computeCmd.Flags().Var(input.GitFlag(), "input-git", "TARGET=REMOTE#REF") - computeCmd.Flags().Var(input.CueFlag(), "input-cue", "CUE") - computeCmd.Flags().Var(input.JSONFlag(), "input-json", "JSON") - computeCmd.Flags().Var(input.YAMLFlag(), "input-yaml", "YAML") - - // Setup (future) --from-* flags - updater, err = dagger.NewInputValue("[...{do:string, ...}]") - if err != nil { - panic(err) - } + computeCmd.Flags().StringSlice("input-string", []string{}, "TARGET=STRING") + computeCmd.Flags().StringSlice("input-dir", []string{}, "TARGET=PATH") + computeCmd.Flags().StringSlice("input-git", []string{}, "TARGET=REMOTE#REF") + computeCmd.Flags().String("input-json", "", "JSON") + computeCmd.Flags().String("input-yaml", "", "YAML") if err := viper.BindPFlags(computeCmd.Flags()); err != nil { panic(err) diff --git a/cmd/dagger/cmd/delete.go b/cmd/dagger/cmd/delete.go new file mode 100644 index 00000000..8dbc5bd8 --- /dev/null +++ b/cmd/dagger/cmd/delete.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a route after taking it offline (WARNING: may destroy infrastructure)", + Args: cobra.NoArgs, + 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(deleteCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/down.go b/cmd/dagger/cmd/down.go new file mode 100644 index 00000000..35aa7d89 --- /dev/null +++ b/cmd/dagger/cmd/down.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "dagger.io/go/cmd/dagger/cmd/common" + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var downCmd = &cobra.Command{ + Use: "down", + Short: "Take a route offline (WARNING: may destroy infrastructure)", + Args: cobra.NoArgs, + 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") + } + + route := common.GetCurrentRoute(ctx, store) + + // TODO: Implement options: --no-cache + if err := route.Down(ctx, nil); err != nil { + lg. + Fatal(). + Err(err). + Str("routeName", route.Name()). + Str("routeId", route.ID()). + Msg("failed to up the route") + } + }, +} + +func init() { + downCmd.Flags().Bool("--no-cache", false, "Disable all run cache") + + if err := viper.BindPFlags(downCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/history.go b/cmd/dagger/cmd/history.go new file mode 100644 index 00000000..05104135 --- /dev/null +++ b/cmd/dagger/cmd/history.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var historyCmd = &cobra.Command{ + Use: "history", + Short: "List past changes to a route", + Args: cobra.NoArgs, + 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(historyCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/input/container.go b/cmd/dagger/cmd/input/container.go new file mode 100644 index 00000000..1ec31366 --- /dev/null +++ b/cmd/dagger/cmd/input/container.go @@ -0,0 +1,33 @@ +package input + +import ( + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var containerCmd = &cobra.Command{ + Use: "container TARGET CONTAINER-IMAGE", + Short: "Add a container image as input artifact", + Args: cobra.ExactArgs(2), + 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()) + + updateRouteInput(ctx, args[0], dagger.DockerInput(args[1])) + }, +} + +func init() { + if err := viper.BindPFlags(containerCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/input/dir.go b/cmd/dagger/cmd/input/dir.go new file mode 100644 index 00000000..809a1401 --- /dev/null +++ b/cmd/dagger/cmd/input/dir.go @@ -0,0 +1,33 @@ +package input + +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 TARGET PATH", + Short: "Add a local directory as input artifact", + Args: cobra.ExactArgs(2), + 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()) + + updateRouteInput(ctx, args[0], dagger.DirInput(args[1], []string{})) + }, +} + +func init() { + if err := viper.BindPFlags(dirCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/input/git.go b/cmd/dagger/cmd/input/git.go new file mode 100644 index 00000000..db0c75cd --- /dev/null +++ b/cmd/dagger/cmd/input/git.go @@ -0,0 +1,38 @@ +package input + +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 TARGET REMOTE REF [SUBDIR]", + Short: "Add a git repository as input artifact", + Args: cobra.RangeArgs(3, 4), + 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()) + + subDir := "" + if len(args) > 3 { + subDir = args[3] + } + + updateRouteInput(ctx, args[0], dagger.GitInput(args[1], args[2], subDir)) + }, +} + +func init() { + if err := viper.BindPFlags(gitCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/input/root.go b/cmd/dagger/cmd/input/root.go new file mode 100644 index 00000000..62150429 --- /dev/null +++ b/cmd/dagger/cmd/input/root.go @@ -0,0 +1,43 @@ +package input + +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: "input", + Short: "Manage a route's inputs", +} + +func init() { + Cmd.AddCommand( + dirCmd, + gitCmd, + containerCmd, + secretCmd, + textCmd, + ) +} + +func updateRouteInput(ctx context.Context, target string, input dagger.Input) { + lg := log.Ctx(ctx) + + store, err := dagger.DefaultStore() + if err != nil { + lg.Fatal().Err(err).Msg("failed to load store") + } + + st := common.GetCurrentRouteState(ctx, store) + st.AddInput(target, input) + + if err := store.UpdateRoute(ctx, st, nil); err != nil { + lg.Fatal().Err(err).Str("routeId", st.ID).Str("routeName", st.Name).Msg("cannot update route") + } + lg.Info().Str("routeId", st.ID).Str("routeName", st.Name).Msg("updated route") +} diff --git a/cmd/dagger/cmd/input/secret.go b/cmd/dagger/cmd/input/secret.go new file mode 100644 index 00000000..7701d046 --- /dev/null +++ b/cmd/dagger/cmd/input/secret.go @@ -0,0 +1,31 @@ +package input + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var secretCmd = &cobra.Command{ + Use: "secret TARGET VALUE", + Short: "Add an encrypted input secret", + Args: cobra.ExactArgs(2), + 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(secretCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/input/text.go b/cmd/dagger/cmd/input/text.go new file mode 100644 index 00000000..3f500680 --- /dev/null +++ b/cmd/dagger/cmd/input/text.go @@ -0,0 +1,33 @@ +package input + +import ( + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var textCmd = &cobra.Command{ + Use: "text TARGET VALUE", + Short: "Add an input text", + Args: cobra.ExactArgs(2), + 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()) + + updateRouteInput(ctx, args[0], dagger.TextInput(args[1])) + }, +} + +func init() { + if err := viper.BindPFlags(textCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/layout/dir.go b/cmd/dagger/cmd/layout/dir.go new file mode 100644 index 00000000..21a1fc4f --- /dev/null +++ b/cmd/dagger/cmd/layout/dir.go @@ -0,0 +1,31 @@ +package layout + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var dirCmd = &cobra.Command{ + Use: "dir PATH", + Short: "Load layout 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()) + + panic("not implemented") + }, +} + +func init() { + if err := viper.BindPFlags(dirCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/layout/file.go b/cmd/dagger/cmd/layout/file.go new file mode 100644 index 00000000..de503531 --- /dev/null +++ b/cmd/dagger/cmd/layout/file.go @@ -0,0 +1,31 @@ +package layout + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var fileCmd = &cobra.Command{ + Use: "file PATH|-", + Short: "Load layout 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/layout/git.go b/cmd/dagger/cmd/layout/git.go new file mode 100644 index 00000000..dd4ca02d --- /dev/null +++ b/cmd/dagger/cmd/layout/git.go @@ -0,0 +1,31 @@ +package layout + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var gitCmd = &cobra.Command{ + Use: "git REMOTE REF [SUBDIR]", + Short: "Load layout from a git package", + Args: cobra.MinimumNArgs(2), + 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(gitCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/layout/package.go b/cmd/dagger/cmd/layout/package.go new file mode 100644 index 00000000..e8705d7a --- /dev/null +++ b/cmd/dagger/cmd/layout/package.go @@ -0,0 +1,31 @@ +package layout + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var packageCmd = &cobra.Command{ + Use: "package PKG", + Short: "Load layout 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/layout/root.go b/cmd/dagger/cmd/layout/root.go new file mode 100644 index 00000000..9f968a5c --- /dev/null +++ b/cmd/dagger/cmd/layout/root.go @@ -0,0 +1,18 @@ +package layout + +import "github.com/spf13/cobra" + +// Cmd exposes the top-level command +var Cmd = &cobra.Command{ + Use: "layout", + Short: "Manage a route's layout", +} + +func init() { + Cmd.AddCommand( + packageCmd, + dirCmd, + gitCmd, + fileCmd, + ) +} diff --git a/cmd/dagger/cmd/list.go b/cmd/dagger/cmd/list.go new file mode 100644 index 00000000..8bf507ce --- /dev/null +++ b/cmd/dagger/cmd/list.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "fmt" + + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List available routes", + Args: cobra.NoArgs, + 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") + } + + routes, err := store.ListRoutes(ctx) + if err != nil { + lg. + Fatal(). + Err(err). + Msg("cannot list routes") + } + + for _, r := range routes { + fmt.Println(r.Name) + } + }, +} + +func init() { + if err := viper.BindPFlags(listCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/login.go b/cmd/dagger/cmd/login.go new file mode 100644 index 00000000..58028c0a --- /dev/null +++ b/cmd/dagger/cmd/login.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Login to Dagger Cloud", + Args: cobra.NoArgs, + 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(loginCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/logout.go b/cmd/dagger/cmd/logout.go new file mode 100644 index 00000000..484015d5 --- /dev/null +++ b/cmd/dagger/cmd/logout.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Logout from Dagger Cloud", + Args: cobra.NoArgs, + 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(logoutCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/new.go b/cmd/dagger/cmd/new.go new file mode 100644 index 00000000..b5a83ae1 --- /dev/null +++ b/cmd/dagger/cmd/new.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "context" + "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 route", + Args: cobra.NoArgs, + 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") + } + + st := &dagger.RouteState{ + Name: getNewRouteName(ctx), + LayoutSource: getLayoutSource(ctx), + } + + err = store.CreateRoute(ctx, st) + if err != nil { + lg.Fatal().Err(err).Msg("failed to create route") + } + lg. + Info(). + Str("routeId", st.ID). + Str("routeName", st.Name). + Msg("route created") + + route, err := dagger.NewRoute(st) + if err != nil { + lg. + Fatal(). + Err(err). + Msg("failed to initialize route") + } + + if viper.GetBool("up") { + common.RouteUp(ctx, route) + } + }, +} + +func getNewRouteName(ctx context.Context) string { + lg := log.Ctx(ctx) + + routeName := viper.GetString("route") + if routeName != "" { + return routeName + } + + 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 +} + +// FIXME: Implement options: --layout-* +func getLayoutSource(ctx context.Context) dagger.Input { + lg := log.Ctx(ctx) + + 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"}) +} + +func init() { + newCmd.Flags().StringP("name", "n", "", "Specify a route name") + newCmd.Flags().BoolP("up", "u", false, "Bring the route online") + + newCmd.Flags().String("layout-dir", "", "Load layout from a local directory") + newCmd.Flags().String("layout-git", "", "Load layout from a git repository") + newCmd.Flags().String("layout-package", "", "Load layout from a cue package") + newCmd.Flags().String("layout-file", "", "Load layout 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/output/dir.go b/cmd/dagger/cmd/output/dir.go new file mode 100644 index 00000000..9de621d3 --- /dev/null +++ b/cmd/dagger/cmd/output/dir.go @@ -0,0 +1,31 @@ +package output + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var dirCmd = &cobra.Command{ + Use: "dir PATH", + Short: "Add a local directory as output artifact", + 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(dirCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/output/root.go b/cmd/dagger/cmd/output/root.go new file mode 100644 index 00000000..057c9a37 --- /dev/null +++ b/cmd/dagger/cmd/output/root.go @@ -0,0 +1,15 @@ +package output + +import "github.com/spf13/cobra" + +// Cmd exposes the top-level command +var Cmd = &cobra.Command{ + Use: "output", + Short: "Manage a route's outputs", +} + +func init() { + Cmd.AddCommand( + dirCmd, + ) +} diff --git a/cmd/dagger/cmd/query.go b/cmd/dagger/cmd/query.go new file mode 100644 index 00000000..8a37bc76 --- /dev/null +++ b/cmd/dagger/cmd/query.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + + "dagger.io/go/cmd/dagger/cmd/common" + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var queryCmd = &cobra.Command{ + Use: "query [EXPR] [flags]", + Short: "Query the contents of a route", + 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()) + + store, err := dagger.DefaultStore() + if err != nil { + lg.Fatal().Err(err).Msg("failed to load store") + } + + route := common.GetCurrentRoute(ctx, store) + + expr := args[0] + + out, err := route.Query(ctx, expr, nil) + if err != nil { + lg. + Fatal(). + Err(err). + Str("routeName", route.Name()). + Str("routeId", route.ID()). + Msg("failed to query route") + } + + fmt.Println(out) + // TODO: Implement options: --no-*, --format, --revision + + }, +} + +func init() { + queryCmd.Flags().String("revision", "latest", "Query a specific version of the route") + queryCmd.Flags().StringP("format", "f", "", "Output format (json|yaml|cue|text|env)") + + queryCmd.Flags().BoolP("--no-input", "I", false, "Exclude inputs from query") + queryCmd.Flags().BoolP("--no-output", "O", false, "Exclude outputs from query") + queryCmd.Flags().BoolP("--no-layout", "L", false, "Exclude outputs from query") + + if err := viper.BindPFlags(queryCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/root.go b/cmd/dagger/cmd/root.go index d791760b..420c2b8e 100644 --- a/cmd/dagger/cmd/root.go +++ b/cmd/dagger/cmd/root.go @@ -4,6 +4,9 @@ import ( "os" "strings" + inputCmd "dagger.io/go/cmd/dagger/cmd/input" + "dagger.io/go/cmd/dagger/cmd/layout" + "dagger.io/go/cmd/dagger/cmd/output" "dagger.io/go/cmd/dagger/logger" "github.com/moby/buildkit/util/appcontext" "github.com/opentracing/opentracing-go" @@ -14,24 +17,28 @@ import ( var rootCmd = &cobra.Command{ Use: "dagger", - Short: "Open-source workflow engine", + Short: "A system for application delivery as code (ADC)", } 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", "debug", "Log level") + rootCmd.PersistentFlags().StringP("route", "r", "", "Select a route") rootCmd.AddCommand( computeCmd, - // Create an env - // Change settings on an env - // View or edit env serti - // settingsCmd, - // Query the state of an env - // getCmd, - // unsetCmd, - // computeCmd, - // listCmd, + newCmd, + listCmd, + queryCmd, + upCmd, + downCmd, + deleteCmd, + historyCmd, + loginCmd, + logoutCmd, + layout.Cmd, + inputCmd.Cmd, + output.Cmd, ) if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { diff --git a/cmd/dagger/cmd/up.go b/cmd/dagger/cmd/up.go new file mode 100644 index 00000000..bffb7722 --- /dev/null +++ b/cmd/dagger/cmd/up.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "dagger.io/go/cmd/dagger/cmd/common" + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var upCmd = &cobra.Command{ + Use: "up", + Short: "Bring a route online with latest layout and inputs", + Args: cobra.NoArgs, + 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") + } + + route := common.GetCurrentRoute(ctx, store) + + // TODO: Implement options: --no-cache + common.RouteUp(ctx, route) + }, +} + +func init() { + newCmd.Flags().Bool("--no-cache", false, "Disable all run cache") + + if err := viper.BindPFlags(upCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/dagger/client.go b/dagger/client.go index 6ae4ebbc..fdf422d0 100644 --- a/dagger/client.go +++ b/dagger/client.go @@ -60,8 +60,8 @@ func NewClient(ctx context.Context, host string) (*Client, error) { }, nil } -// FIXME: return completed *Env, instead of *compiler.Value -func (c *Client) Compute(ctx context.Context, env *Env) (*compiler.Value, error) { +// FIXME: return completed *Route, instead of *compiler.Value +func (c *Client) Up(ctx context.Context, route *Route) (*compiler.Value, error) { lg := log.Ctx(ctx) eg, gctx := errgroup.WithContext(ctx) @@ -78,7 +78,7 @@ func (c *Client) Compute(ctx context.Context, env *Env) (*compiler.Value, error) outr, outw := io.Pipe() eg.Go(func() error { defer outw.Close() - return c.buildfn(gctx, env, events, outw) + return c.buildfn(gctx, route, events, outw) }) // Spawn output retriever @@ -95,11 +95,11 @@ func (c *Client) Compute(ctx context.Context, env *Env) (*compiler.Value, error) return out, compiler.Err(eg.Wait()) } -func (c *Client) buildfn(ctx context.Context, env *Env, ch chan *bk.SolveStatus, w io.WriteCloser) error { +func (c *Client) buildfn(ctx context.Context, route *Route, ch chan *bk.SolveStatus, w io.WriteCloser) error { lg := log.Ctx(ctx) // Scan local dirs to grant access - localdirs := env.LocalDirs() + localdirs := route.LocalDirs() for label, dir := range localdirs { abs, err := filepath.Abs(dir) if err != nil { @@ -132,24 +132,24 @@ func (c *Client) buildfn(ctx context.Context, env *Env, ch chan *bk.SolveStatus, s := NewSolver(c.c, gw, ch) lg.Debug().Msg("loading configuration") - if err := env.Update(ctx, s); err != nil { + if err := route.LoadLayout(ctx, s); err != nil { return nil, err } // Compute output overlay - lg.Debug().Msg("computing env") - if err := env.Compute(ctx, s); err != nil { + lg.Debug().Msg("computing route") + if err := route.Up(ctx, s, nil); err != nil { return nil, err } - // Export env to a cue directory + // Export route to a cue directory // FIXME: this should be elsewhere - lg.Debug().Msg("exporting env") - span, _ := opentracing.StartSpanFromContext(ctx, "Env.Export") + lg.Debug().Msg("exporting route") + span, _ := opentracing.StartSpanFromContext(ctx, "Route.Export") defer span.Finish() st := llb.Scratch().File( - llb.Mkfile("state.cue", 0600, env.State().JSON()), + llb.Mkfile("state.cue", 0600, route.State().JSON()), llb.WithCustomName("[internal] serializing state to JSON"), ) ref, err := s.Solve(ctx, st) @@ -178,7 +178,7 @@ func (c *Client) buildfn(ctx context.Context, env *Env, ch chan *bk.SolveStatus, func (c *Client) outputfn(ctx context.Context, r io.Reader) (*compiler.Value, error) { lg := log.Ctx(ctx) - // FIXME: merge this into env output. + // FIXME: merge this into route output. out := compiler.EmptyStruct() tr := tar.NewReader(r) diff --git a/dagger/compiler/compiler_test.go b/dagger/compiler/compiler_test.go index 37c298ee..c4ff093f 100644 --- a/dagger/compiler/compiler_test.go +++ b/dagger/compiler/compiler_test.go @@ -2,55 +2,34 @@ package compiler import ( "testing" + + "github.com/stretchr/testify/require" ) // Test that a non-existing field is detected correctly func TestFieldNotExist(t *testing.T) { c := &Compiler{} root, err := c.Compile("test.cue", `foo: "bar"`) - if err != nil { - t.Fatal(err) - } - if v := root.Get("foo"); !v.Exists() { - // value should exist - t.Fatal(v) - } - if v := root.Get("bar"); v.Exists() { - // value should NOT exist - t.Fatal(v) - } + require.NoError(t, err) + require.True(t, root.Get("foo").Exists()) + require.False(t, root.Get("bar").Exists()) } // Test that a non-existing definition is detected correctly func TestDefNotExist(t *testing.T) { c := &Compiler{} root, err := c.Compile("test.cue", `foo: #bla: "bar"`) - if err != nil { - t.Fatal(err) - } - if v := root.Get("foo.#bla"); !v.Exists() { - // value should exist - t.Fatal(v) - } - if v := root.Get("foo.#nope"); v.Exists() { - // value should NOT exist - t.Fatal(v) - } + require.NoError(t, err) + require.True(t, root.Get("foo.#bla").Exists()) + require.False(t, root.Get("foo.#nope").Exists()) } func TestJSON(t *testing.T) { c := &Compiler{} v, err := c.Compile("", `foo: hello: "world"`) - if err != nil { - t.Fatal(err) - } - b1 := v.JSON() - if string(b1) != `{"foo":{"hello":"world"}}` { - t.Fatal(b1) - } + require.NoError(t, err) + require.Equal(t, `{"foo":{"hello":"world"}}`, string(v.JSON())) + // Reproduce a bug where Value.Get().JSON() ignores Get() - b2 := v.Get("foo").JSON() - if string(b2) != `{"hello":"world"}` { - t.Fatal(b2) - } + require.Equal(t, `{"hello":"world"}`, string(v.Get("foo").JSON())) } diff --git a/dagger/compiler/value.go b/dagger/compiler/value.go index 0314cfdc..d0ee9477 100644 --- a/dagger/compiler/value.go +++ b/dagger/compiler/value.go @@ -123,11 +123,6 @@ func (v *Value) Int64() (int64, error) { return v.val.Int64() } -func (v *Value) SourceUnsafe() string { - s, _ := v.SourceString() - return s -} - // Proxy function to the underlying cue.Value func (v *Value) Path() cue.Path { return v.val.Path() @@ -236,12 +231,6 @@ func (v *Value) Source() ([]byte, error) { return cueformat.Node(v.val.Eval().Syntax()) } -// Return cue source for this value, as a Go string -func (v *Value) SourceString() (string, error) { - b, err := v.Source() - return string(b), err -} - func (v *Value) IsEmptyStruct() bool { if st, err := v.Struct(); err == nil { if st.Len() == 0 { diff --git a/dagger/dagger_test.go b/dagger/dagger_test.go deleted file mode 100644 index 2e78eecb..00000000 --- a/dagger/dagger_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package dagger - -import ( - "testing" - - "dagger.io/go/dagger/compiler" -) - -func TestLocalDirs(t *testing.T) { - env := mkEnv(t, - `#compute: [ - { - do: "local" - dir: "bar" - } - ]`, - `dir: #compute: [ - { - do: "local" - dir: "foo" - } - ]`, - ) - dirs := env.LocalDirs() - if len(dirs) != 2 { - t.Fatal(dirs) - } - if _, ok := dirs["foo"]; !ok { - t.Fatal(dirs) - } - if _, ok := dirs["bar"]; !ok { - t.Fatal(dirs) - } -} - -func mkEnv(t *testing.T, updater, input string) *Env { - env, err := NewEnv() - if err != nil { - t.Fatal(err) - } - u, err := compiler.Compile("updater.cue", updater) - if err != nil { - t.Fatal(err) - } - if err := env.SetUpdater(u); err != nil { - t.Fatal(err) - } - i, err := compiler.Compile("input.cue", input) - if err != nil { - t.Fatal(err) - } - if err := env.SetInput(i); err != nil { - t.Fatal(err) - } - return env -} diff --git a/dagger/input.go b/dagger/input.go index bff4442e..d37ee82b 100644 --- a/dagger/input.go +++ b/dagger/input.go @@ -2,297 +2,188 @@ package dagger import ( "encoding/json" - "errors" "fmt" - "net/url" - "os" - "strings" - - "cuelang.org/go/cue" - "github.com/spf13/pflag" "dagger.io/go/dagger/compiler" - - "go.mozilla.org/sops" - "go.mozilla.org/sops/decrypt" ) -// A mutable cue value with an API suitable for user inputs, -// such as command-line flag parsing. -type InputValue struct { - root *compiler.Value +// An input is a value or artifact supplied by the user. +// +// - A value is any structured data which can be encoded as cue. +// +// - An artifact is a piece of data, like a source code checkout, +// binary bundle, docker image, database backup etc. +// +// Artifacts can be passed as inputs, generated dynamically from +// other inputs, and received as outputs. +// Under the hood, an artifact is encoded as a LLB pipeline, and +// attached to the cue configuration as a +// +type InputType string + +const ( + InputTypeDir InputType = "dir" + InputTypeGit InputType = "git" + InputTypeDocker InputType = "docker" + InputTypeText InputType = "text" + InputTypeJSON InputType = "json" + InputTypeYAML InputType = "yaml" +) + +type Input struct { + Type InputType `json:"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"` } -func (iv *InputValue) Value() *compiler.Value { - return iv.root +func (i Input) Compile() (*compiler.Value, error) { + switch i.Type { + case InputTypeDir: + return i.Dir.Compile() + case InputTypeGit: + return i.Git.Compile() + case InputTypeDocker: + return i.Docker.Compile() + case InputTypeText: + return i.Text.Compile() + case InputTypeJSON: + return i.JSON.Compile() + case InputTypeYAML: + return i.YAML.Compile() + case "": + return nil, fmt.Errorf("input has not been set") + default: + return nil, fmt.Errorf("unsupported input type: %s", i.Type) + } } -func (iv *InputValue) String() string { - s, _ := iv.root.SourceString() - return s +// An input artifact loaded from a local directory +func DirInput(path string, include []string) Input { + return Input{ + Type: InputTypeDir, + Dir: &dirInput{ + Path: path, + Include: include, + }, + } } -func NewInputValue(base interface{}) (*InputValue, error) { - root, err := compiler.Compile("base", base) +type dirInput struct { + Path string `json:"path,omitempty"` + Include []string `json:"include,omitempty"` +} + +func (dir dirInput) Compile() (*compiler.Value, error) { + // FIXME: serialize an intermediate struct, instead of generating cue source + includeLLB, err := json.Marshal(dir.Include) if err != nil { return nil, err } - return &InputValue{ - root: root, - }, nil + llb := fmt.Sprintf( + `#compute: [{do:"local",dir:"%s", include:%s}]`, + dir.Path, + includeLLB, + ) + return compiler.Compile("", llb) } -func (iv *InputValue) Set(s string, enc func(string) (interface{}, error)) error { - // Split from eg. 'foo.bar={bla:"bla"}` - k, vRaw := splitkv(s) - v, err := enc(vRaw) - if err != nil { - return err - } - root, err := iv.root.MergePath(v, k) - if err != nil { - return err - } - iv.root = root - return nil +// An input artifact loaded from a git repository +type gitInput struct { + Remote string `json:"remote,omitempty"` + Ref string `json:"ref,omitempty"` + Dir string `json:"dir,omitempty"` } -// Adapter to receive string values from pflag -func (iv *InputValue) StringFlag() pflag.Value { - return stringFlag{ - iv: iv, +func GitInput(remote, ref, dir string) Input { + return Input{ + Type: InputTypeGit, + Git: &gitInput{ + Remote: remote, + Ref: ref, + Dir: dir, + }, } } -type stringFlag struct { - iv *InputValue +func (git gitInput) Compile() (*compiler.Value, error) { + panic("NOT IMPLEMENTED") } -func (sf stringFlag) Set(s string) error { - return sf.iv.Set(s, func(s string) (interface{}, error) { - return s, nil - }) -} - -func (sf stringFlag) String() string { - return sf.iv.String() -} - -func (sf stringFlag) Type() string { - return "STRING" -} - -// DIR FLAG -// Receive a local directory path and translate it into a component -func (iv *InputValue) DirFlag(include ...string) pflag.Value { - if include == nil { - include = []string{} - } - return dirFlag{ - iv: iv, - include: include, +// An input artifact loaded from a docker container +func DockerInput(ref string) Input { + return Input{ + Type: InputTypeDocker, + Docker: &dockerInput{ + Ref: ref, + }, } } -type dirFlag struct { - iv *InputValue - include []string +type dockerInput struct { + Ref string `json:"ref,omitempty"` } -func (f dirFlag) Set(s string) error { - return f.iv.Set(s, func(s string) (interface{}, error) { - // FIXME: this is a hack because cue API can't merge into a list - include, err := json.Marshal(f.include) - if err != nil { - return nil, err - } - return compiler.Compile("", fmt.Sprintf( - `#compute: [{do:"local",dir:"%s", include:%s}]`, - s, - include, - )) - }) +func (i dockerInput) Compile() (*compiler.Value, error) { + panic("NOT IMPLEMENTED") } -func (f dirFlag) String() string { - return f.iv.String() -} - -func (f dirFlag) Type() string { - return "PATH" -} - -// GIT FLAG -// Receive a git repository reference and translate it into a component -func (iv *InputValue) GitFlag() pflag.Value { - return gitFlag{ - iv: iv, +// An input value encoded as text +func TextInput(data string) Input { + return Input{ + Type: InputTypeText, + Text: &textInput{ + Data: data, + }, } } -type gitFlag struct { - iv *InputValue +type textInput struct { + Data string `json:"data,omitempty"` } -func (f gitFlag) Set(s string) error { - return f.iv.Set(s, func(s string) (interface{}, error) { - u, err := url.Parse(s) - if err != nil { - return nil, fmt.Errorf("invalid git url") - } - ref := u.Fragment // eg. #main - u.Fragment = "" - remote := u.String() - - return compiler.Compile("", fmt.Sprintf( - `#compute: [{do:"fetch-git", remote:"%s", ref:"%s"}]`, - remote, - ref, - )) - }) +func (i textInput) Compile() (*compiler.Value, error) { + return compiler.Compile("", fmt.Sprintf("%q", i.Data)) } -func (f gitFlag) String() string { - return f.iv.String() -} - -func (f gitFlag) Type() string { - return "REMOTE,REF" -} - -// SOURCE FLAG -// Adapter to receive a simple source description and translate it to a loader script. -// For example 'git+https://github.com/cuelang/cue#master` -> [{do:"git",remote:"https://github.com/cuelang/cue",ref:"master"}] - -func (iv *InputValue) SourceFlag() pflag.Value { - return sourceFlag{ - iv: iv, +// An input value encoded as JSON +func JSONInput(data string) Input { + return Input{ + Type: InputTypeJSON, + JSON: &jsonInput{ + Data: data, + }, } } -type sourceFlag struct { - iv *InputValue +type jsonInput struct { + // Marshalled JSON data + Data string `json:"data,omitempty"` } -func (f sourceFlag) Set(s string) error { - return f.iv.Set(s, func(s string) (interface{}, error) { - u, err := url.Parse(s) - if err != nil { - return nil, err - } - switch u.Scheme { - case "", "file": - return compiler.Compile( - "source", - // FIXME: include only cue files as a shortcut. Make this configurable somehow. - fmt.Sprintf(`[{do:"local",dir:"%s",include:["*.cue","cue.mod"]}]`, u.Host+u.Path), - ) - default: - return nil, fmt.Errorf("unsupported source scheme: %q", u.Scheme) - } - }) +func (i jsonInput) Compile() (*compiler.Value, error) { + return compiler.DecodeJSON("", []byte(i.Data)) } -func (f sourceFlag) String() string { - return f.iv.String() -} - -func (f sourceFlag) Type() string { - return "PATH | file://PATH | git+ssh://HOST/PATH | git+https://HOST/PATH" -} - -// RAW CUE FLAG -// Adapter to receive raw cue values from pflag -func (iv *InputValue) CueFlag() pflag.Value { - return cueFlag{ - iv: iv, +// An input value encoded as YAML +func YAMLInput(data string) Input { + return Input{ + Type: InputTypeYAML, + YAML: &yamlInput{ + Data: data, + }, } } -type cueFlag struct { - iv *InputValue +type yamlInput struct { + // Marshalled YAML data + Data string `json:"data,omitempty"` } -func (f cueFlag) Set(s string) error { - return f.iv.Set(s, func(s string) (interface{}, error) { - return compiler.Compile("cue input", s) - }) -} - -func (f cueFlag) String() string { - return f.iv.String() -} - -func (f cueFlag) Type() string { - return "CUE" -} - -func (iv *InputValue) YAMLFlag() pflag.Value { - return fileFlag{ - iv: iv, - format: "yaml", - } -} - -func (iv *InputValue) JSONFlag() pflag.Value { - return fileFlag{ - iv: iv, - format: "json", - } -} - -type fileFlag struct { - format string - iv *InputValue -} - -func (f fileFlag) Set(s string) error { - return f.iv.Set(s, func(s string) (interface{}, error) { - content, err := os.ReadFile(s) - if err != nil { - return nil, err - } - - plaintext, err := decrypt.Data(content, f.format) - if err != nil && !errors.Is(err, sops.MetadataNotFound) { - return nil, fmt.Errorf("unable to decrypt %q: %w", s, err) - } - - if len(plaintext) > 0 { - content = plaintext - } - - switch f.format { - case "json": - return compiler.DecodeJSON(s, content) - case "yaml": - return compiler.DecodeYAML(s, content) - default: - panic("unsupported file format") - } - }) -} - -func (f fileFlag) String() string { - return f.iv.String() -} - -func (f fileFlag) Type() string { - return strings.ToUpper(f.format) -} - -// UTILITIES - -func splitkv(kv string) (cue.Path, string) { - parts := strings.SplitN(kv, "=", 2) - if len(parts) == 2 { - if parts[0] == "." || parts[0] == "" { - return cue.MakePath(), parts[1] - } - return cue.ParsePath(parts[0]), parts[1] - } - if len(parts) == 1 { - return cue.MakePath(), parts[0] - } - return cue.MakePath(), "" +func (i yamlInput) Compile() (*compiler.Value, error) { + return compiler.DecodeYAML("", []byte(i.Data)) } diff --git a/dagger/input_test.go b/dagger/input_test.go index 836be8e2..a8482946 100644 --- a/dagger/input_test.go +++ b/dagger/input_test.go @@ -2,30 +2,21 @@ package dagger import ( "testing" + + "github.com/stretchr/testify/require" ) -func TestEnvInputFlag(t *testing.T) { - env, err := NewEnv() - if err != nil { - t.Fatal(err) +func TestInputDir(t *testing.T) { + st := &RouteState{ + LayoutSource: DirInput("/tmp/source", []string{}), } + require.NoError(t, st.AddInput("www.source", DirInput(".", []string{}))) - input, err := NewInputValue(`{}`) - if err != nil { - t.Fatal(err) - } - if err := input.DirFlag().Set("www.source=."); err != nil { - t.Fatal(err) - } - if err := env.SetInput(input.Value()); err != nil { - t.Fatal(err) - } + route, err := NewRoute(st) + require.NoError(t, err) - localdirs := env.LocalDirs() - if len(localdirs) != 1 { - t.Fatal(localdirs) - } - if dir, ok := localdirs["."]; !ok || dir != "." { - t.Fatal(localdirs) - } + localdirs := route.LocalDirs() + require.Len(t, localdirs, 2) + require.Contains(t, localdirs, ".") + require.Contains(t, localdirs, "/tmp/source") } diff --git a/dagger/pipeline.go b/dagger/pipeline.go index 6ea5034e..c7ec55d3 100644 --- a/dagger/pipeline.go +++ b/dagger/pipeline.go @@ -84,7 +84,11 @@ func ops(code ...*compiler.Value) ([]*compiler.Value, error) { ops = append(ops, xops...) } else { // 4. error - return nil, fmt.Errorf("not executable: %s", x.SourceUnsafe()) + source, err := x.Source() + if err != nil { + panic(err) + } + return nil, fmt.Errorf("not executable: %s", source) } } return ops, nil diff --git a/dagger/env.go b/dagger/route.go similarity index 59% rename from dagger/env.go rename to dagger/route.go index a99534ed..788cfc3c 100644 --- a/dagger/env.go +++ b/dagger/route.go @@ -18,17 +18,59 @@ import ( "github.com/rs/zerolog/log" ) -type Env struct { +// Contents of a route serialized to a file +type RouteState struct { + // Globally unique route ID + ID string `json:"id,omitempty"` + + // Human-friendly route name. + // A route may have more than one name. + // FIXME: store multiple names? + Name string `json:"name,omitempty"` + + // Cue module containing the route layout + // The input's top-level artifact is used as a module directory. + LayoutSource Input `json:"layout,omitempty"` + + Inputs []inputKV `json:"inputs,omitempty"` +} + +type inputKV struct { + Key string `json:"key,omitempty"` + Value Input `json:"value,omitempty"` +} + +func (r *RouteState) AddInput(key string, value Input) error { + r.Inputs = append(r.Inputs, inputKV{Key: key, Value: value}) + return nil +} + +// 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 (r *RouteState) RemoveInputs(key string) error { + newInputs := make([]inputKV, 0, len(r.Inputs)) + for _, i := range r.Inputs { + if i.Key == key { + continue + } + newInputs = append(newInputs, i) + } + r.Inputs = newInputs + + return nil +} + +type Route struct { + st *RouteState + // Env boot script, eg. `[{do:"local",dir:"."}]` // FIXME: rename to 'update' (script to update the env config) // FIXME: embed update script in base as '#update' ? - // FIXME: simplify Env by making it single layer? Each layer is one env. + // FIXME: simplify Env by making it single layer? Each layer is one r. - // How to update the base configuration - updater *compiler.Value - - // Layer 1: base configuration - base *compiler.Value + // Layer 1: layout configuration + layout *compiler.Value // Layer 2: user inputs input *compiler.Value @@ -36,67 +78,82 @@ type Env struct { // Layer 3: computed values output *compiler.Value - // All layers merged together: base + input + output + // All layers merged together: layout + input + output state *compiler.Value } -func (env *Env) Updater() *compiler.Value { - return env.updater -} - -// Set the updater script for this environment. -func (env *Env) SetUpdater(v *compiler.Value) error { - if v == nil { - var err error - v, err = compiler.Compile("", "[]") - if err != nil { - return err - } - } - env.updater = v - return nil -} - -func NewEnv() (*Env, error) { +func NewRoute(st *RouteState) (*Route, error) { empty := compiler.EmptyStruct() - env := &Env{ - base: empty, + r := &Route{ + st: st, + layout: empty, input: empty, output: empty, } - if err := env.mergeState(); err != nil { + + // Prepare inputs + for _, input := range st.Inputs { + v, err := input.Value.Compile() + if err != nil { + return nil, err + } + if input.Key == "" { + r.input, err = r.input.Merge(v) + } else { + r.input, err = r.input.MergeTarget(v, input.Key) + } + if err != nil { + return nil, err + } + } + if err := r.mergeState(); err != nil { return nil, err } - if err := env.SetUpdater(nil); err != nil { - return nil, err - } - return env, nil + + return r, nil } -func (env *Env) State() *compiler.Value { - return env.state +func (r *Route) ID() string { + return r.st.ID } -func (env *Env) Input() *compiler.Value { - return env.input +func (r *Route) Name() string { + return r.st.Name } -func (env *Env) SetInput(i *compiler.Value) error { - if i == nil { - i = compiler.EmptyStruct() - } - env.input = i - return env.mergeState() +func (r *Route) LayoutSource() Input { + return r.st.LayoutSource } -// Update the base configuration -func (env *Env) Update(ctx context.Context, s Solver) error { - span, ctx := opentracing.StartSpanFromContext(ctx, "Env.Update") +func (r *Route) Layout() *compiler.Value { + return r.layout +} + +func (r *Route) Input() *compiler.Value { + return r.input +} + +func (r *Route) Output() *compiler.Value { + return r.output +} + +func (r *Route) State() *compiler.Value { + return r.state +} + +// LoadLayout loads the layout +func (r *Route) LoadLayout(ctx context.Context, s Solver) error { + span, ctx := opentracing.StartSpanFromContext(ctx, "route.Update") defer span.Finish() + layoutSource, err := r.st.LayoutSource.Compile() + if err != nil { + return err + } + p := NewPipeline("[internal] source", s, nil) // execute updater script - if err := p.Do(ctx, env.updater); err != nil { + if err := p.Do(ctx, layoutSource); err != nil { return err } @@ -105,28 +162,21 @@ func (env *Env) Update(ctx context.Context, s Solver) error { stdlib.Path: stdlib.FS, "/": p.FS(), } - base, err := compiler.Build(sources) + layout, err := compiler.Build(sources) if err != nil { - return fmt.Errorf("base config: %w", err) + return fmt.Errorf("layout config: %w", err) } - env.base = base + r.layout = layout + // Commit - return env.mergeState() -} - -func (env *Env) Base() *compiler.Value { - return env.base -} - -func (env *Env) Output() *compiler.Value { - return env.output + return r.mergeState() } // Scan all scripts in the environment for references to local directories (do:"local"), // and return all referenced directory names. // This is used by clients to grant access to local directories when they are referenced // by user-specified scripts. -func (env *Env) LocalDirs() map[string]string { +func (r *Route) LocalDirs() map[string]string { dirs := map[string]string{} localdirs := func(code ...*compiler.Value) { Analyze( @@ -135,6 +185,7 @@ func (env *Env) LocalDirs() map[string]string { if err != nil { return err } + // FIXME: merge Env into Route, or fix the linter error if do != "local" { return nil } @@ -150,19 +201,24 @@ func (env *Env) LocalDirs() map[string]string { } // 1. Scan the environment state // FIXME: use a common `flow` instance to avoid rescanning the tree. - inst := env.state.CueInst() + inst := r.state.CueInst() flow := cueflow.New(&cueflow.Config{}, inst, newTaskFunc(inst, noOpRunner)) for _, t := range flow.Tasks() { v := compiler.Wrap(t.Value(), inst) localdirs(v.Get("#compute")) } - // 2. Scan the environment updater - localdirs(env.Updater()) + + // 2. Scan the layout + layout, err := r.st.LayoutSource.Compile() + if err != nil { + panic(err) + } + localdirs(layout) return dirs } // FIXME: this is just a 3-way merge. Add var args to compiler.Value.Merge. -func (env *Env) mergeState() error { +func (r *Route) mergeState() error { // FIXME: make this cleaner in *compiler.Value by keeping intermediary instances // FIXME: state.CueInst() must return an instance with the same // contents as state.v, for the purposes of cueflow. @@ -175,15 +231,15 @@ func (env *Env) mergeState() error { err error ) - stateInst, err = stateInst.Fill(env.base.Cue()) + stateInst, err = stateInst.Fill(r.layout.Cue()) if err != nil { return fmt.Errorf("merge base & input: %w", err) } - stateInst, err = stateInst.Fill(env.input.Cue()) + stateInst, err = stateInst.Fill(r.input.Cue()) if err != nil { return fmt.Errorf("merge base & input: %w", err) } - stateInst, err = stateInst.Fill(env.output.Cue()) + stateInst, err = stateInst.Fill(r.output.Cue()) if err != nil { return fmt.Errorf("merge output with base & input: %w", err) } @@ -191,22 +247,24 @@ func (env *Env) mergeState() error { state = compiler.Wrap(stateInst.Value(), stateInst) // commit - env.state = state + r.state = state return nil } -// Compute missing values in env configuration, and write them to state. -func (env *Env) Compute(ctx context.Context, s Solver) error { - span, ctx := opentracing.StartSpanFromContext(ctx, "Env.Compute") +type UpOpts struct{} + +// Up missing values in env configuration, and write them to state. +func (r *Route) Up(ctx context.Context, s Solver, _ *UpOpts) error { + span, ctx := opentracing.StartSpanFromContext(ctx, "r.Compute") defer span.Finish() lg := log.Ctx(ctx) // Cueflow cue instance - inst := env.state.CueInst() + inst := r.state.CueInst() // Reset the output - env.output = compiler.EmptyStruct() + r.output = compiler.EmptyStruct() // Cueflow config flowCfg := &cueflow.Config{ @@ -226,7 +284,7 @@ func (env *Env) Compute(ctx context.Context, s Solver) error { } // Merge task value into output var err error - env.output, err = env.output.MergePath(t.Value(), t.Path()) + r.output, err = r.output.MergePath(t.Value(), t.Path()) if err != nil { lg. Error(). @@ -244,13 +302,25 @@ func (env *Env) Compute(ctx context.Context, s Solver) error { } { - span, _ := opentracing.StartSpanFromContext(ctx, "Env.Compute: merge state") + span, _ := opentracing.StartSpanFromContext(ctx, "merge state") defer span.Finish() - return env.mergeState() + return r.mergeState() } } +type DownOpts struct{} + +func (r *Route) Down(ctx context.Context, _ *DownOpts) error { + panic("NOT IMPLEMENTED") +} + +func (r *Route) Query(ctx context.Context, expr interface{}, o *QueryOpts) (*compiler.Value, error) { + panic("NOT IMPLEMENTED") +} + +type QueryOpts struct{} + func newTaskFunc(inst *cue.Instance, runner cueflow.RunnerFunc) cueflow.TaskFunc { return func(flowVal cue.Value) (cueflow.Runner, error) { v := compiler.Wrap(flowVal, inst) diff --git a/dagger/store.go b/dagger/store.go new file mode 100644 index 00000000..4566126b --- /dev/null +++ b/dagger/store.go @@ -0,0 +1,226 @@ +package dagger + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path" + "sync" + + "github.com/google/uuid" +) + +var ( + ErrRouteExist = errors.New("route already exists") + ErrRouteNotExist = errors.New("route doesn't exist") +) + +const ( + defaultStoreRoot = "$HOME/.config/dagger/routes" +) + +type Store struct { + root string + + l sync.RWMutex + + routes map[string]*RouteState + + // Various indices for fast lookups + routesByName map[string]*RouteState + routesByPath map[string]*RouteState + pathsByRoute map[string][]string +} + +func NewStore(root string) (*Store, error) { + store := &Store{ + root: root, + routes: make(map[string]*RouteState), + routesByName: make(map[string]*RouteState), + routesByPath: make(map[string]*RouteState), + pathsByRoute: make(map[string][]string), + } + return store, store.loadAll() +} + +func DefaultStore() (*Store, error) { + return NewStore(os.ExpandEnv(defaultStoreRoot)) +} + +func (s *Store) routePath(name string) string { + return path.Join(s.root, name, "route.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.loadRoute(f.Name()); err != nil { + return err + } + } + + return nil +} + +func (s *Store) loadRoute(name string) error { + data, err := os.ReadFile(s.routePath(name)) + if err != nil { + return err + } + var st RouteState + if err := json.Unmarshal(data, &st); err != nil { + return err + } + s.indexRoute(&st) + return nil +} + +func (s *Store) syncRoute(r *RouteState) error { + p := s.routePath(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, 0644); err != nil { + return err + } + + s.reindexRoute(r) + + return nil +} + +func (s *Store) indexRoute(r *RouteState) { + s.routes[r.ID] = r + s.routesByName[r.Name] = r + + mapPath := func(i Input) { + if i.Type != InputTypeDir { + return + } + s.routesByPath[i.Dir.Path] = r + s.pathsByRoute[r.ID] = append(s.pathsByRoute[r.ID], i.Dir.Path) + } + + mapPath(r.LayoutSource) + for _, i := range r.Inputs { + mapPath(i.Value) + } +} + +func (s *Store) deindexRoute(id string) { + r, ok := s.routes[id] + if !ok { + return + } + delete(s.routes, r.ID) + delete(s.routesByName, r.Name) + + for _, p := range s.pathsByRoute[r.ID] { + delete(s.routesByPath, p) + } + delete(s.pathsByRoute, r.ID) +} + +func (s *Store) reindexRoute(r *RouteState) { + s.deindexRoute(r.ID) + s.indexRoute(r) +} + +func (s *Store) CreateRoute(ctx context.Context, st *RouteState) error { + s.l.Lock() + defer s.l.Unlock() + + if _, ok := s.routesByName[st.Name]; ok { + return fmt.Errorf("%s: %w", st.Name, ErrRouteExist) + } + + st.ID = uuid.New().String() + return s.syncRoute(st) +} + +type UpdateOpts struct{} + +func (s *Store) UpdateRoute(ctx context.Context, r *RouteState, o *UpdateOpts) error { + s.l.Lock() + defer s.l.Unlock() + + return s.syncRoute(r) +} + +type DeleteOpts struct{} + +func (s *Store) DeleteRoute(ctx context.Context, r *RouteState, o *DeleteOpts) error { + s.l.Lock() + defer s.l.Unlock() + + if err := os.Remove(s.routePath(r.Name)); err != nil { + return err + } + s.deindexRoute(r.ID) + return nil +} + +func (s *Store) LookupRouteByID(ctx context.Context, id string) (*RouteState, error) { + s.l.RLock() + defer s.l.RUnlock() + + st, ok := s.routes[id] + if !ok { + return nil, fmt.Errorf("%s: %w", id, ErrRouteNotExist) + } + return st, nil +} + +func (s *Store) LookupRouteByName(ctx context.Context, name string) (*RouteState, error) { + s.l.RLock() + defer s.l.RUnlock() + + st, ok := s.routesByName[name] + if !ok { + return nil, fmt.Errorf("%s: %w", name, ErrRouteNotExist) + } + return st, nil +} + +func (s *Store) LookupRouteByPath(ctx context.Context, path string) (*RouteState, error) { + s.l.RLock() + defer s.l.RUnlock() + + st, ok := s.routesByPath[path] + if !ok { + return nil, fmt.Errorf("%s: %w", path, ErrRouteNotExist) + } + return st, nil +} + +func (s *Store) ListRoutes(ctx context.Context) ([]*RouteState, error) { + s.l.RLock() + defer s.l.RUnlock() + + routes := make([]*RouteState, 0, len(s.routes)) + + for _, st := range s.routes { + routes = append(routes, st) + } + + return routes, nil +} diff --git a/dagger/store_test.go b/dagger/store_test.go new file mode 100644 index 00000000..9602a336 --- /dev/null +++ b/dagger/store_test.go @@ -0,0 +1,99 @@ +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.LookupRouteByName(ctx, "notexist") + require.Error(t, err) + require.True(t, errors.Is(err, ErrRouteNotExist)) + + st := &RouteState{ + Name: "test", + } + require.NoError(t, store.CreateRoute(ctx, st)) + + checkRoutes := func(store *Store) { + r, err := store.LookupRouteByID(ctx, st.ID) + require.NoError(t, err) + require.NotNil(t, r) + require.Equal(t, "test", r.Name) + + r, err = store.LookupRouteByName(ctx, "test") + require.NoError(t, err) + require.NotNil(t, r) + require.Equal(t, "test", r.Name) + + routes, err := store.ListRoutes(ctx) + require.NoError(t, err) + require.Len(t, routes, 1) + require.Equal(t, "test", routes[0].Name) + } + + checkRoutes(store) + + // Reload the routes from disk and check again + newStore, err := NewStore(root) + require.NoError(t, err) + checkRoutes(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 := &RouteState{ + Name: "test", + } + require.NoError(t, st.AddInput("foo", DirInput("/test/path", []string{}))) + require.NoError(t, store.CreateRoute(ctx, st)) + + // Lookup by path + r, err := store.LookupRouteByPath(ctx, "/test/path") + require.NoError(t, err) + require.NotNil(t, r) + require.Equal(t, st.ID, r.ID) + + // Add a new path + require.NoError(t, st.AddInput("bar", DirInput("/test/anotherpath", []string{}))) + require.NoError(t, store.UpdateRoute(ctx, st, nil)) + + // Lookup by the previous path + r, err = store.LookupRouteByPath(ctx, "/test/path") + require.NoError(t, err) + require.Equal(t, st.ID, r.ID) + + // Lookup by the new path + r, err = store.LookupRouteByPath(ctx, "/test/anotherpath") + require.NoError(t, err) + require.Equal(t, st.ID, r.ID) + + // Remove a path + require.NoError(t, st.RemoveInputs("foo")) + require.NoError(t, store.UpdateRoute(ctx, st, nil)) + + // Lookup by the removed path should fail + _, err = store.LookupRouteByPath(ctx, "/test/path") + require.Error(t, err) + + // Lookup by the other path should still work + _, err = store.LookupRouteByPath(ctx, "/test/anotherpath") + require.NoError(t, err) +} diff --git a/go.mod b/go.mod index 5c8f3c2f..a1d7cfe3 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/containerd/console v1.0.1 github.com/docker/distribution v2.7.1+incompatible github.com/emicklei/proto v1.9.0 // indirect + github.com/google/uuid v1.2.0 // indirect github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db github.com/moby/buildkit v0.8.2 @@ -18,6 +19,7 @@ require ( github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.5.1 // indirect github.com/tonistiigi/fsutil v0.0.0-20201103201449-0834f99b7b85 github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea github.com/uber/jaeger-client-go v2.25.0+incompatible diff --git a/go.sum b/go.sum index 91ad7d37..fac2bece 100644 --- a/go.sum +++ b/go.sum @@ -659,6 +659,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE= github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= diff --git a/tests/test.sh b/tests/test.sh index 7a8296b7..01169718 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -129,7 +129,7 @@ test::exec(){ test::one "Exec: env valid" --exit=0 --stdout={} \ "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute "$d"/exec/env/valid test::one "Exec: env with overlay" --exit=0 \ - "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute --input-cue 'bar: "overlay environment"' "$d"/exec/env/overlay + "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute --input-string 'bar=overlay environment' "$d"/exec/env/overlay test::one "Exec: non existent dir" --exit=0 --stdout={} \ "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute "$d"/exec/dir/doesnotexist @@ -230,16 +230,15 @@ test::input() { "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute "$d"/input/simple test::one "Input: simple input" --exit=0 --stdout='{"in":"foobar","test":"received: foobar"}' \ - "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute --input-cue 'in: "foobar"' "$d"/input/simple + "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute --input-string 'in=foobar' "$d"/input/simple test::one "Input: default values" --exit=0 --stdout='{"in":"default input","test":"received: default input"}' \ "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute "$d"/input/default test::one "Input: override default value" --exit=0 --stdout='{"in":"foobar","test":"received: foobar"}' \ - "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute --input-cue 'in: "foobar"' "$d"/input/default + "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute --input-string 'in=foobar' "$d"/input/default } - test::subdir() { test::one "Subdir: simple usage" --exit=0 --stdout='{"hello":"world"}' \ "$dagger" "${DAGGER_BINARY_ARGS[@]}" compute "$d"/subdir/simple