diff --git a/cmd/dagger/cmd/down.go b/cmd/dagger/cmd/down.go index b62667b9..64e7f692 100644 --- a/cmd/dagger/cmd/down.go +++ b/cmd/dagger/cmd/down.go @@ -33,7 +33,7 @@ var downCmd = &cobra.Command{ // TODO: Implement options: --no-cache if err := route.Down(ctx, nil); err != nil { - lg.Fatal().Err(err).Str("route-name", routeName).Str("route-id", route.ID).Msg("failed to up the route") + lg.Fatal().Err(err).Str("route-name", routeName).Str("route-id", route.ID()).Msg("failed to up the route") } }, } diff --git a/cmd/dagger/cmd/new.go b/cmd/dagger/cmd/new.go index f317caca..6c8197a3 100644 --- a/cmd/dagger/cmd/new.go +++ b/cmd/dagger/cmd/new.go @@ -37,12 +37,12 @@ var newCmd = &cobra.Command{ if err != nil { lg.Fatal().Err(err).Msg("failed to create route") } - lg.Info().Str("route-id", route.ID).Str("route-name", routeName).Msg("created route") + lg.Info().Str("route-id", route.ID()).Str("route-name", routeName).Msg("created route") if upRoute { - lg.Info().Str("route-id", route.ID).Msg("bringing route online") + lg.Info().Str("route-id", route.ID()).Msg("bringing route online") if err := route.Up(ctx, nil); err != nil { - lg.Fatal().Err(err).Str("route-id", route.ID).Msg("failed to create route") + lg.Fatal().Err(err).Str("route-id", route.ID()).Msg("failed to create route") } } }, diff --git a/cmd/dagger/cmd/query.go b/cmd/dagger/cmd/query.go index e60f6a47..91b0775a 100644 --- a/cmd/dagger/cmd/query.go +++ b/cmd/dagger/cmd/query.go @@ -36,7 +36,7 @@ var queryCmd = &cobra.Command{ out, err := route.Query(ctx, expr, nil) if err != nil { - lg.Fatal().Err(err).Str("route-name", routeName).Str("route-id", route.ID).Msg("failed to query route") + lg.Fatal().Err(err).Str("route-name", routeName).Str("route-id", route.ID()).Msg("failed to query route") } fmt.Println(out) diff --git a/cmd/dagger/cmd/up.go b/cmd/dagger/cmd/up.go index c04655f1..d5c9cf3f 100644 --- a/cmd/dagger/cmd/up.go +++ b/cmd/dagger/cmd/up.go @@ -33,7 +33,7 @@ var upCmd = &cobra.Command{ // TODO: Implement options: --no-cache if err := route.Up(ctx, nil); err != nil { - lg.Fatal().Err(err).Str("route-name", routeName).Str("route-id", route.ID).Msg("failed to up the route") + lg.Fatal().Err(err).Str("route-name", routeName).Str("route-id", route.ID()).Msg("failed to up the route") } }, } diff --git a/dagger/deprecated_input.go b/dagger/deprecated_input.go new file mode 100644 index 00000000..bff4442e --- /dev/null +++ b/dagger/deprecated_input.go @@ -0,0 +1,298 @@ +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 +} + +func (iv *InputValue) Value() *compiler.Value { + return iv.root +} + +func (iv *InputValue) String() string { + s, _ := iv.root.SourceString() + return s +} + +func NewInputValue(base interface{}) (*InputValue, error) { + root, err := compiler.Compile("base", base) + if err != nil { + return nil, err + } + return &InputValue{ + root: root, + }, nil +} + +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 +} + +// Adapter to receive string values from pflag +func (iv *InputValue) StringFlag() pflag.Value { + return stringFlag{ + iv: iv, + } +} + +type stringFlag struct { + iv *InputValue +} + +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, + } +} + +type dirFlag struct { + iv *InputValue + include []string +} + +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 (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, + } +} + +type gitFlag struct { + iv *InputValue +} + +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 (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, + } +} + +type sourceFlag struct { + iv *InputValue +} + +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 (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, + } +} + +type cueFlag struct { + iv *InputValue +} + +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(), "" +} diff --git a/dagger/input.go b/dagger/input.go index bff4442e..b58aacb9 100644 --- a/dagger/input.go +++ b/dagger/input.go @@ -2,297 +2,144 @@ 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 Input interface { + // Compile to a cue value which can be merged into a route config + Compile() (*compiler.Value, error) } -func (iv *InputValue) Value() *compiler.Value { - return iv.root +// An input artifact loaded from a local directory +func DirInput(path string, include []string) Input { + return &dirInput{ + Type: "dir", + Path: path, + Include: include, + } } -func (iv *InputValue) String() string { - s, _ := iv.root.SourceString() - return s +type dirInput struct { + Type string + Path string + Include []string } -func NewInputValue(base interface{}) (*InputValue, error) { - root, err := compiler.Compile("base", base) +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( + `[{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 { + Type string + Remote string + Ref string + Dir string } -// 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 &gitInput{ + Type: "git", + 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 &dockerInput{ + Type: "docker", + Ref: ref, } } -type dirFlag struct { - iv *InputValue - include []string +type dockerInput struct { + Type string + Ref string } -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 &textInput{ + Type: "text", + Data: data, } } -type gitFlag struct { - iv *InputValue +type textInput struct { + Type string + Data string } -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) { + panic("NOT IMPLEMENTED") } -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 &jsonInput{ + Type: "json", + Data: data, } } -type sourceFlag struct { - iv *InputValue +type jsonInput struct { + Type string + // Marshalled JSON data + Data string } -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) { + panic("NOT IMPLEMENTED") } -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 &yamlInput{ + Type: "yaml", + Data: data, } } -type cueFlag struct { - iv *InputValue +type yamlInput struct { + Type string + // Marshalled YAML data + Data string } -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) { + panic("NOT IMPLEMENTED") } diff --git a/dagger/route.go b/dagger/route.go index 3b1fed93..5d02e0db 100644 --- a/dagger/route.go +++ b/dagger/route.go @@ -8,16 +8,67 @@ import ( // A deployment route type Route struct { + st routeState +} + +func (r Route) ID() string { + return r.st.ID +} + +func (r Route) Name() string { + return r.st.Name +} + +func (r Route) LayoutSource() Input { + return r.st.LayoutSource +} + +func (r *Route) SetLayoutSource(ctx context.Context, src Input) error { + r.st.LayoutSource = src + return nil +} + +func (r *Route) AddInput(ctx context.Context, key string, value Input) error { + r.st.Inputs = append(r.st.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 *Route) RemoveInputs(ctx context.Context, key string) error { + panic("NOT IMPLEMENTED") +} + +// Contents of a route serialized to a file +type routeState struct { // Globally unique route ID ID string // Human-friendly route name. // A route may have more than one name. + // FIXME: store multiple names? Name string + + // Cue module containing the route layout + // The input's top-level artifact is used as a module directory. + LayoutSource Input + + Inputs []inputKV +} + +type inputKV struct { + Key string + Value Input } func CreateRoute(ctx context.Context, name string, o *CreateOpts) (*Route, error) { - panic("NOT IMPLEMENTED") + return &Route{ + st: routeState{ + ID: "FIXME", + Name: name, + }, + }, nil } type CreateOpts struct{} @@ -57,66 +108,3 @@ func (r *Route) Query(ctx context.Context, expr interface{}, o *QueryOpts) (*com } type QueryOpts struct{} - -func (r *Route) SetLayout(ctx context.Context, a *Artifact) error { - panic("NOT IMPLEMENTED") -} - -func (r *Route) Layout() (*Artifact, error) { - panic("NOT IMPLEMENTED") -} - -func (r *Route) AddInputArtifact(ctx context.Context, target string, a *Artifact) error { - panic("NOT IMPLEMENTED") -} - -func (r *Route) AddInputValue(ctx context.Context, target string, v *compiler.Value) error { - panic("NOT IMPLEMENTED") -} - -// FIXME: how does remove work? Does it require a specific file layout? -func (r *Route) RemoveInputs(ctx context.Context, target string) error { - panic("NOT IMPLEMENTED") -} - -// FIXME: connect outputs to auto-export values and artifacts. - -// An artifact is a piece of data, like a source code checkout, -// binary bundle, container 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 Artifact struct { - llb interface{} -} - -func Dir(path string, include []string) *Artifact { - var llb struct { - Do string - Include []string - } - llb.Do = "local" - llb.Include = include - return &Artifact{ - llb: llb, - } -} - -func Git(remote, ref, dir string) *Artifact { - panic("NOT IMPLEMENTED") -} - -func Container(ref string) *Artifact { - panic("NOT IMPLEMENTED") -} - -func LLB(code interface{}) *Artifact { - panic("NOT IMPLEMENTED") -} - -// FIXME: manage base -// FIXME: manage inputs -// FIXME: manage outputs