Merge pull request #459 from aluzzardi/gitflow-ux

gitflow
This commit is contained in:
Andrea Luzzardi 2021-05-25 15:38:18 -07:00 committed by GitHub
commit 93d7bb08e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1092 additions and 1258 deletions

View File

@ -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
}
}

View File

@ -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
}

View File

@ -1 +0,0 @@
../../examples/hello-world

View File

@ -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

View File

@ -2,65 +2,84 @@ package common
import (
"context"
"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 CurrentWorkspace(ctx context.Context) *state.Workspace {
lg := log.Ctx(ctx)
if workspacePath := viper.GetString("workspace"); workspacePath != "" {
workspace, err := state.Open(ctx, workspacePath)
if err != nil {
lg.
Fatal().
Err(err).
Str("path", workspacePath).
Msg("failed to open workspace")
}
return workspace
}
workspace, err := state.Current(ctx)
if err != nil {
lg.
Fatal().
Err(err).
Msg("failed to determine current workspace")
}
return workspace
}
func CurrentEnvironmentState(ctx context.Context, workspace *state.Workspace) *state.State {
lg := log.Ctx(ctx)
environmentName := viper.GetString("environment")
if environmentName != "" {
st, err := store.LookupEnvironmentByName(ctx, environmentName)
st, err := workspace.Get(ctx, environmentName)
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")
}
st, err := store.LookupEnvironmentByPath(ctx, wd)
environments, err := workspace.List(ctx)
if err != nil {
lg.
Fatal().
Err(err).
Str("environmentPath", wd).
Msg("failed to lookup environment by path")
Msg("failed to list environments")
}
if len(st) == 0 {
if len(environments) == 0 {
lg.
Fatal().
Err(err).
Str("environmentPath", wd).
Msg("no environments match the current directory")
Msg("no environments")
}
if len(st) > 1 {
environments := []string{}
for _, s := range st {
environments = append(environments, s.Name)
if len(environments) > 1 {
envNames := []string{}
for _, e := range environments {
envNames = append(envNames, e.Name)
}
lg.
Fatal().
Err(err).
Str("environmentPath", wd).
Strs("environments", environments).
Msg("multiple environments match the current directory, select one with `--environment`")
Strs("environments", envNames).
Msg("multiple environments available in the workspace, select one with `--environment`")
}
return st[0]
return environments[0]
}
// 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)

View File

@ -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,16 @@ var computeCmd = &cobra.Command{
lg := logger.New()
ctx := lg.WithContext(cmd.Context())
st := &dagger.EnvironmentState{
ID: uuid.New().String(),
st := &state.State{
Name: "FIXME",
PlanSource: dagger.DirInput(args[0], []string{"*.cue", "cue.mod"}),
Path: args[0],
Plan: 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 +57,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 +70,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 +101,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 +124,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 +142,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")
}

View File

@ -1,31 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete an environment 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)
}
}

49
cmd/dagger/cmd/init.go Normal file
View File

@ -0,0 +1,49 @@
package cmd
import (
"os"
"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("workspace")
if dir == "" {
cwd, err := os.Getwd()
if err != nil {
lg.
Fatal().
Err(err).
Msg("failed to get current working dir")
}
dir = cwd
}
_, err := state.Init(ctx, dir)
if err != nil {
lg.Fatal().Err(err).Msg("failed to initialize workspace")
}
},
}
func init() {
if err := viper.BindPFlags(initCmd.Flags()); err != nil {
panic(err)
}
}

View File

@ -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]))
},
}

View File

@ -1,8 +1,12 @@
package input
import (
"path/filepath"
"strings"
"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"
)
@ -22,7 +26,24 @@ var dirCmd = &cobra.Command{
lg := logger.New()
ctx := lg.WithContext(cmd.Context())
updateEnvironmentInput(ctx, args[0], dagger.DirInput(args[1], []string{}))
p, err := filepath.Abs(args[1])
if err != nil {
lg.Fatal().Err(err).Str("path", args[1]).Msg("unable to resolve path")
}
workspace := common.CurrentWorkspace(ctx)
if !strings.HasPrefix(p, workspace.Path) {
lg.Fatal().Err(err).Str("path", args[1]).Msg("dir is outside the workspace")
}
p, err = filepath.Rel(workspace.Path, p)
if err != nil {
lg.Fatal().Err(err).Str("path", args[1]).Msg("unable to resolve path")
}
if !strings.HasPrefix(p, ".") {
p = "./" + p
}
updateEnvironmentInput(ctx, args[0], state.DirInput(p, []string{}))
},
}

View File

@ -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))
},
}

View File

@ -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])),
)
},
}

View File

@ -10,6 +10,7 @@ import (
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger"
"dagger.io/go/dagger/compiler"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -30,16 +31,11 @@ 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)
workspace := common.CurrentWorkspace(ctx)
environment := common.CurrentEnvironmentState(ctx, workspace)
lg = lg.With().
Str("environmentName", environment.Name).
Str("environmentId", environment.ID).
Str("environment", environment.Name).
Logger()
c, err := dagger.NewClient(ctx, "", false)
@ -90,9 +86,9 @@ var listCmd = &cobra.Command{
},
}
func isUserSet(env *dagger.EnvironmentState, val *compiler.Value) bool {
for _, i := range env.Inputs {
if val.Path().String() == i.Key {
func isUserSet(env *state.State, val *compiler.Value) bool {
for key := range env.Inputs {
if val.Path().String() == key {
return true
}
}

View File

@ -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,16 @@ 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)
workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
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 := workspace.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 {

View File

@ -1,14 +1,20 @@
package input
import (
"fmt"
"syscall"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger/state"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
)
var secretCmd = &cobra.Command{
Use: "secret TARGET VALUE",
Use: "secret <TARGET> [-f] [<VALUE|PATH>]",
Short: "Add an encrypted input secret",
Args: cobra.ExactArgs(2),
Args: cobra.RangeArgs(1, 2),
PreRun: func(cmd *cobra.Command, args []string) {
// Fix Viper bug for duplicate flags:
// https://github.com/spf13/viper/issues/233
@ -17,14 +23,35 @@ var secretCmd = &cobra.Command{
}
},
Run: func(cmd *cobra.Command, args []string) {
// lg := logger.New()
// ctx := lg.WithContext(cmd.Context())
lg := logger.New()
ctx := lg.WithContext(cmd.Context())
panic("not implemented")
var secret string
if len(args) == 1 {
// No value specified: prompt terminal
fmt.Print("Secret: ")
data, err := term.ReadPassword(syscall.Stdin)
if err != nil {
lg.Fatal().Err(err).Msg("unable to read secret from terminal")
}
fmt.Println("")
secret = string(data)
} else {
// value specified: read it
secret = readInput(ctx, args[1])
}
updateEnvironmentInput(
ctx,
args[0],
state.SecretInput(secret),
)
},
}
func init() {
secretCmd.Flags().BoolP("file", "f", false, "Read value from file")
if err := viper.BindPFlags(secretCmd.Flags()); err != nil {
panic(err)
}

View File

@ -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])),
)
},
}

View File

@ -3,7 +3,6 @@ package input
import (
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -23,17 +22,13 @@ 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)
workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
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 := workspace.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")
},
}

View File

@ -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])),
)
},
}

View File

@ -1,7 +1,6 @@
package cmd
import (
"context"
"fmt"
"os"
"os/user"
@ -9,9 +8,8 @@ import (
"strings"
"text/tabwriter"
"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"
)
@ -30,12 +28,9 @@ 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")
}
environments, err := store.ListEnvironments(ctx)
workspace := common.CurrentWorkspace(ctx)
environments, err := workspace.List(ctx)
if err != nil {
lg.
Fatal().
@ -43,47 +38,15 @@ var listCmd = &cobra.Command{
Msg("cannot list environments")
}
environmentID := getCurrentEnvironmentID(ctx, store)
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 {
line = fmt.Sprintf("%s- active environment", line)
}
defer w.Flush()
for _, e := range environments {
line := fmt.Sprintf("%s\t%s\t", e.Name, formatPath(e.Path))
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 {
lg := log.Ctx(ctx)
wd, err := os.Getwd()
if err != nil {
lg.Warn().Err(err).Msg("cannot get current working directory")
return ""
}
st, err := store.LookupEnvironmentByPath(ctx, wd)
if err != nil {
// Ignore error
return ""
}
if len(st) == 1 {
return st[0].ID
}
return ""
}
func formatPath(p string) string {
usr, err := user.Current()
if err != nil {
@ -99,15 +62,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"
}

View File

@ -1,24 +1,15 @@
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),
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
// Fix Viper bug for duplicate flags:
// https://github.com/spf13/viper/issues/233
@ -29,119 +20,22 @@ var newCmd = &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")
}
workspace := common.CurrentWorkspace(ctx)
if viper.GetString("environment") != "" {
lg.
Fatal().
Msg("cannot use option -d,--environment for this command")
Msg("cannot use option -e,--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 {
name := args[0]
if _, err := workspace.Create(ctx, name); 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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -30,16 +30,11 @@ 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)
workspace := common.CurrentWorkspace(ctx)
state := common.CurrentEnvironmentState(ctx, workspace)
lg = lg.With().
Str("environmentName", state.Name).
Str("environmentId", state.ID).
Str("environment", state.Name).
Logger()
cuePath := cue.MakePath()

View File

@ -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,17 +33,16 @@ func init() {
}
rootCmd.AddCommand(
computeCmd,
initCmd,
newCmd,
computeCmd,
listCmd,
queryCmd,
upCmd,
downCmd,
deleteCmd,
historyCmd,
loginCmd,
logoutCmd,
plan.Cmd,
input.Cmd,
output.Cmd,
versionCmd,

View File

@ -3,7 +3,6 @@ package cmd
import (
"dagger.io/go/cmd/dagger/cmd/common"
"dagger.io/go/cmd/dagger/logger"
"dagger.io/go/dagger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -23,15 +22,13 @@ 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 {
workspace := common.CurrentWorkspace(ctx)
st := common.CurrentEnvironmentState(ctx, workspace)
result := common.EnvironmentUp(ctx, st, viper.GetBool("no-cache"))
st.Computed = result.Computed().JSON().PrettyString()
if err := workspace.Save(ctx, st); err != nil {
lg.Fatal().Err(err).Msg("failed to update environment")
}
},

View File

@ -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)

View File

@ -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())
}

View File

@ -10,6 +10,7 @@ import (
"cuelang.org/go/cue"
cueflow "cuelang.org/go/tools/flow"
"dagger.io/go/dagger/compiler"
"dagger.io/go/dagger/state"
"dagger.io/go/stdlib"
"github.com/opentracing/opentracing-go"
@ -19,7 +20,7 @@ import (
)
type Environment struct {
state *EnvironmentState
state *state.State
// Layer 1: plan configuration
plan *compiler.Value
@ -31,7 +32,7 @@ type Environment struct {
computed *compiler.Value
}
func NewEnvironment(st *EnvironmentState) (*Environment, error) {
func NewEnvironment(st *state.State) (*Environment, error) {
e := &Environment{
state: st,
@ -41,15 +42,15 @@ func NewEnvironment(st *EnvironmentState) (*Environment, error) {
}
// Prepare inputs
for _, input := range st.Inputs {
v, err := input.Value.Compile()
for key, input := range st.Inputs {
v, err := input.Compile(st)
if err != nil {
return nil, err
}
if input.Key == "" {
if key == "" {
err = e.input.FillPath(cue.MakePath(), v)
} else {
err = e.input.FillPath(cue.ParsePath(input.Key), v)
err = e.input.FillPath(cue.ParsePath(key), v)
}
if err != nil {
return nil, err
@ -59,16 +60,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 +85,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
}
@ -106,7 +103,7 @@ func (e *Environment) LoadPlan(ctx context.Context, s Solver) error {
}
plan, err := compiler.Build(sources)
if err != nil {
return fmt.Errorf("plan config: %w", err)
return fmt.Errorf("plan config: %w", compiler.Err(err))
}
e.plan = plan
@ -159,7 +156,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)
}

View File

@ -0,0 +1,24 @@
package dagger
import (
"testing"
"dagger.io/go/dagger/state"
"github.com/stretchr/testify/require"
)
func TestLocalDirs(t *testing.T) {
st := &state.State{
Path: "/tmp/source",
Plan: "/tmp/source/plan",
}
require.NoError(t, st.SetInput("www.source", state.DirInput("/", []string{})))
environment, err := NewEnvironment(st)
require.NoError(t, err)
localdirs := environment.LocalDirs()
require.Len(t, localdirs, 2)
require.Contains(t, localdirs, "/")
require.Contains(t, localdirs, "/tmp/source/plan")
}

View File

@ -1,22 +0,0 @@
package dagger
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestInputDir(t *testing.T) {
st := &EnvironmentState{
PlanSource: DirInput("/tmp/source", []string{}),
}
require.NoError(t, st.SetInput("www.source", DirInput("/", []string{})))
environment, err := NewEnvironment(st)
require.NoError(t, err)
localdirs := environment.LocalDirs()
require.Len(t, localdirs, 2)
require.Contains(t, localdirs, "/")
require.Contains(t, localdirs, "/tmp/source")
}

159
dagger/keychain/encrypt.go Normal file
View File

@ -0,0 +1,159 @@
package keychain
import (
"context"
"fmt"
"os"
"time"
"go.mozilla.org/sops/v3"
sopsaes "go.mozilla.org/sops/v3/aes"
sopsage "go.mozilla.org/sops/v3/age"
"go.mozilla.org/sops/v3/cmd/sops/common"
sopskeys "go.mozilla.org/sops/v3/keys"
sopsyaml "go.mozilla.org/sops/v3/stores/yaml"
"go.mozilla.org/sops/v3/version"
)
var (
cipher = sopsaes.NewCipher()
)
// setupEnv: hack to inject a SOPS env var for age
func setupEnv() error {
p, err := Path()
if err != nil {
return err
}
return os.Setenv("SOPS_AGE_KEY_FILE", p)
}
// Encrypt data using SOPS with the AGE backend, using the provided public key
func Encrypt(ctx context.Context, path string, plaintext []byte, key string) ([]byte, error) {
if err := setupEnv(); err != nil {
return nil, err
}
store := &sopsyaml.Store{}
branches, err := store.LoadPlainFile(plaintext)
if err != nil {
return nil, err
}
ageKeys, err := sopsage.MasterKeysFromRecipients(key)
if err != nil {
return nil, err
}
ageMasterKeys := make([]sopskeys.MasterKey, 0, len(ageKeys))
for _, k := range ageKeys {
ageMasterKeys = append(ageMasterKeys, k)
}
var group sops.KeyGroup
group = append(group, ageMasterKeys...)
tree := sops.Tree{
Branches: branches,
Metadata: sops.Metadata{
KeyGroups: []sops.KeyGroup{group},
EncryptedSuffix: "secret",
Version: version.Version,
},
FilePath: path,
}
// Generate a data key
dataKey, errs := tree.GenerateDataKey()
if len(errs) > 0 {
return nil, fmt.Errorf("error encrypting the data key with one or more master keys: %v", errs)
}
err = common.EncryptTree(common.EncryptTreeOpts{
DataKey: dataKey, Tree: &tree, Cipher: cipher,
})
if err != nil {
return nil, err
}
return store.EmitEncryptedFile(tree)
}
// Reencrypt a file with new content using the same keys
func Reencrypt(_ context.Context, path string, plaintext []byte) ([]byte, error) {
if err := setupEnv(); err != nil {
return nil, err
}
current, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Load the encrypted file
store := &sopsyaml.Store{}
tree, err := store.LoadEncryptedFile(current)
if err != nil {
return nil, err
}
// Update the file with the new data
newBranches, err := store.LoadPlainFile(plaintext)
if err != nil {
return nil, err
}
tree.Branches = newBranches
// Re-encrypt the file
key, err := tree.Metadata.GetDataKey()
if err != nil {
return nil, err
}
err = common.EncryptTree(common.EncryptTreeOpts{
DataKey: key, Tree: &tree, Cipher: cipher,
})
if err != nil {
return nil, err
}
return store.EmitEncryptedFile(tree)
}
// Decrypt data using sops
func Decrypt(_ context.Context, encrypted []byte) ([]byte, error) {
if err := setupEnv(); err != nil {
return nil, err
}
store := &sopsyaml.Store{}
// Load SOPS file and access the data key
tree, err := store.LoadEncryptedFile(encrypted)
if err != nil {
return nil, err
}
key, err := tree.Metadata.GetDataKey()
if err != nil {
return nil, err
}
// Decrypt the tree
mac, err := tree.Decrypt(key, cipher)
if err != nil {
return nil, err
}
// Compute the hash of the cleartext tree and compare it with
// the one that was stored in the document. If they match,
// integrity was preserved
originalMac, err := cipher.Decrypt(
tree.Metadata.MessageAuthenticationCode,
key,
tree.Metadata.LastModified.Format(time.RFC3339),
)
if err != nil {
return nil, err
}
if originalMac != mac {
return nil, fmt.Errorf("failed to verify data integrity. expected mac %q, got %q", originalMac, mac)
}
return store.EmitPlainFile(tree.Branches)
}

96
dagger/keychain/keys.go Normal file
View File

@ -0,0 +1,96 @@
package keychain
import (
"context"
"errors"
"fmt"
"os"
"os/user"
"path"
"path/filepath"
"time"
"filippo.io/age"
"github.com/rs/zerolog/log"
)
func Path() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
return path.Join(usr.HomeDir, ".dagger", "keys.txt"), nil
}
func Default(ctx context.Context) (string, error) {
keys, err := List(ctx)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return Generate(ctx)
}
return "", err
}
if len(keys) == 0 {
return "", errors.New("no identities found in the keys file")
}
return keys[0].Recipient().String(), nil
}
func Generate(ctx context.Context) (string, error) {
keysFile, err := Path()
if err != nil {
return "", err
}
k, err := age.GenerateX25519Identity()
if err != nil {
return "", fmt.Errorf("internal error: %v", err)
}
if err := os.MkdirAll(filepath.Dir(keysFile), 0755); err != nil {
return "", err
}
f, err := os.OpenFile(keysFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return "", fmt.Errorf("failed to open keys file %q: %v", keysFile, err)
}
defer f.Close()
fmt.Fprintf(f, "# created: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(f, "# public key: %s\n", k.Recipient())
fmt.Fprintf(f, "%s\n", k)
pubkey := k.Recipient().String()
log.Ctx(ctx).Debug().Str("publicKey", pubkey).Msg("generating keypair")
return pubkey, nil
}
func List(ctx context.Context) ([]*age.X25519Identity, error) {
keysFile, err := Path()
if err != nil {
return nil, err
}
f, err := os.Open(keysFile)
if err != nil {
return nil, fmt.Errorf("failed to open keys file file %q: %w", keysFile, err)
}
ids, err := age.ParseIdentities(f)
if err != nil {
return nil, fmt.Errorf("failed to parse input: %w", err)
}
keys := make([]*age.X25519Identity, 0, len(ids))
for _, id := range ids {
key, ok := ids[0].(*age.X25519Identity)
if !ok {
return nil, fmt.Errorf("internal error: unexpected identity type: %T", id)
}
keys = append(keys, key)
}
return keys, nil
}

View File

@ -1,56 +0,0 @@
package dagger
// Contents of an environment serialized to a file
type EnvironmentState struct {
// Globally unique environment ID
ID string `json:"id,omitempty"`
// 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"`
// User Inputs
Inputs []inputKV `json:"inputs,omitempty"`
// Computed values
Computed string `json:"output,omitempty"`
}
type inputKV struct {
Key string `json:"key,omitempty"`
Value Input `json:"value,omitempty"`
}
func (s *EnvironmentState) SetInput(key string, value Input) error {
for i, inp := range s.Inputs {
if inp.Key != key {
continue
}
// Remove existing inputs with the same key
s.Inputs = append(s.Inputs[:i], s.Inputs[i+1:]...)
}
s.Inputs = append(s.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 (s *EnvironmentState) RemoveInputs(key string) error {
newInputs := make([]inputKV, 0, len(s.Inputs))
for _, i := range s.Inputs {
if i.Key == key {
continue
}
newInputs = append(newInputs, i)
}
s.Inputs = newInputs
return nil
}

View File

@ -1,10 +1,12 @@
package dagger
package state
import (
"encoding/json"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"cuelang.org/go/cue"
@ -23,64 +25,44 @@ import (
// 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"
InputTypeFile InputType = "file"
InputTypeEmpty InputType = ""
)
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"`
File *fileInput `json:"file,omitempty"`
Dir *dirInput `yaml:"dir,omitempty"`
Git *gitInput `yaml:"git,omitempty"`
Docker *dockerInput `yaml:"docker,omitempty"`
Secret *secretInput `yaml:"secret,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) {
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 InputTypeFile:
return i.File.Compile()
case "":
return nil, fmt.Errorf("input has not been set")
func (i Input) Compile(state *State) (*compiler.Value, error) {
switch {
case i.Dir != nil:
return i.Dir.Compile(state)
case i.Git != nil:
return i.Git.Compile(state)
case i.Docker != nil:
return i.Docker.Compile(state)
case i.Text != nil:
return i.Text.Compile(state)
case i.Secret != nil:
return i.Secret.Compile(state)
case i.JSON != nil:
return i.JSON.Compile(state)
case i.YAML != nil:
return i.YAML.Compile(state)
case i.File != nil:
return i.File.Compile(state)
default:
return nil, fmt.Errorf("unsupported input type: %s", i.Type)
return nil, fmt.Errorf("input has not been set")
}
}
// 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{
Path: path,
Include: include,
@ -93,7 +75,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 +88,18 @@ func (dir dirInput) Compile() (*compiler.Value, error) {
return nil, err
}
}
p := dir.Path
if !filepath.IsAbs(p) {
p = filepath.Clean(path.Join(state.Workspace, dir.Path))
}
if !strings.HasPrefix(p, state.Workspace) {
return nil, fmt.Errorf("%q is outside the workspace", dir.Path)
}
llb := fmt.Sprintf(
`#up: [{do:"local",dir:"%s", include:%s}]`,
dir.Path,
p,
includeLLB,
)
return compiler.Compile("", llb)
@ -123,7 +114,6 @@ type gitInput struct {
func GitInput(remote, ref, dir string) Input {
return Input{
Type: InputTypeGit,
Git: &gitInput{
Remote: remote,
Ref: ref,
@ -132,7 +122,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
@ -148,7 +138,6 @@ func (git gitInput) Compile() (*compiler.Value, error) {
// An input artifact loaded from a docker container
func DockerInput(ref string) Input {
return Input{
Type: InputTypeDocker,
Docker: &dockerInput{
Ref: ref,
},
@ -159,69 +148,68 @@ 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")
}
// An input value encoded as text
func TextInput(data string) Input {
i := textInput(data)
return Input{
Type: InputTypeText,
Text: &textInput{
Data: data,
},
Text: &i,
}
}
type textInput struct {
Data string `json:"data,omitempty"`
type textInput string
func (i textInput) Compile(_ *State) (*compiler.Value, error) {
return compiler.Compile("", fmt.Sprintf("%q", i))
}
func (i textInput) Compile() (*compiler.Value, error) {
return compiler.Compile("", fmt.Sprintf("%q", i.Data))
// A secret input value
func SecretInput(data string) Input {
i := secretInput(data)
return Input{
Secret: &i,
}
}
type secretInput string
func (i secretInput) Compile(_ *State) (*compiler.Value, error) {
return compiler.Compile("", fmt.Sprintf("%q", i))
}
// An input value encoded as JSON
func JSONInput(data string) Input {
i := jsonInput(data)
return Input{
Type: InputTypeJSON,
JSON: &jsonInput{
Data: data,
},
JSON: &i,
}
}
type jsonInput struct {
// Marshalled JSON data
Data string `json:"data,omitempty"`
}
type jsonInput string
func (i jsonInput) Compile() (*compiler.Value, error) {
return compiler.DecodeJSON("", []byte(i.Data))
func (i jsonInput) Compile(_ *State) (*compiler.Value, error) {
return compiler.DecodeJSON("", []byte(i))
}
// An input value encoded as YAML
func YAMLInput(data string) Input {
i := yamlInput(data)
return Input{
Type: InputTypeYAML,
YAML: &yamlInput{
Data: data,
},
YAML: &i,
}
}
type yamlInput struct {
// Marshalled YAML data
Data string `json:"data,omitempty"`
}
type yamlInput string
func (i yamlInput) Compile() (*compiler.Value, error) {
return compiler.DecodeYAML("", []byte(i.Data))
func (i yamlInput) Compile(_ *State) (*compiler.Value, error) {
return compiler.DecodeYAML("", []byte(i))
}
func FileInput(data string) Input {
return Input{
Type: InputTypeFile,
File: &fileInput{
Path: data,
},
@ -232,7 +220,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

46
dagger/state/state.go Normal file
View File

@ -0,0 +1,46 @@
package state
// Contents of an environment serialized to a file
type State struct {
// State path
Path string `yaml:"-"`
// Workspace path
Workspace string `yaml:"-"`
// Plan path
Plan string `yaml:"-"`
// Human-friendly environment name.
// A environment may have more than one name.
// FIXME: store multiple names?
Name string `yaml:"name,omitempty"`
// User Inputs
Inputs map[string]Input `yaml:"inputs,omitempty"`
// Computed values
Computed string `yaml:"-"`
}
// 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.Plan, []string{"*.cue", "cue.mod"})
}
func (s *State) SetInput(key string, value Input) error {
if s.Inputs == nil {
s.Inputs = make(map[string]Input)
}
s.Inputs[key] = 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 (s *State) RemoveInputs(key string) error {
delete(s.Inputs, key)
return nil
}

270
dagger/state/workspace.go Normal file
View File

@ -0,0 +1,270 @@
package state
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"dagger.io/go/dagger/keychain"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
var (
ErrNotInit = errors.New("not initialized")
ErrAlreadyInit = errors.New("already initialized")
ErrNotExist = errors.New("environment doesn't exist")
ErrExist = errors.New("environment already exists")
)
const (
daggerDir = ".dagger"
envDir = "env"
stateDir = "state"
planDir = "plan"
manifestFile = "values.yaml"
computedFile = "computed.json"
)
type Workspace struct {
Path string
}
func Init(ctx context.Context, dir string) (*Workspace, error) {
root, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
daggerRoot := path.Join(root, daggerDir)
if err := os.Mkdir(daggerRoot, 0755); err != nil {
if errors.Is(err, os.ErrExist) {
return nil, ErrAlreadyInit
}
return nil, err
}
if err := os.Mkdir(path.Join(daggerRoot, envDir), 0755); err != nil {
return nil, err
}
return &Workspace{
Path: root,
}, nil
}
func Open(ctx context.Context, dir string) (*Workspace, error) {
_, err := os.Stat(path.Join(dir, daggerDir))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotInit
}
return nil, err
}
root, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
return &Workspace{
Path: root,
}, nil
}
func Current(ctx context.Context) (*Workspace, error) {
current, err := os.Getwd()
if err != nil {
return nil, err
}
// Walk every parent directory to find .dagger
for {
_, err := os.Stat(path.Join(current, daggerDir, envDir))
if err == nil {
return Open(ctx, current)
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
return nil, ErrNotInit
}
func (w *Workspace) envPath(name string) string {
return path.Join(w.Path, daggerDir, envDir, name)
}
func (w *Workspace) List(ctx context.Context) ([]*State, error) {
var (
environments = []*State{}
err error
)
files, err := os.ReadDir(path.Join(w.Path, daggerDir, envDir))
if err != nil {
return nil, err
}
for _, f := range files {
if !f.IsDir() {
continue
}
st, err := w.Get(ctx, f.Name())
if err != nil {
log.
Ctx(ctx).
Err(err).
Str("name", f.Name()).
Msg("failed to load environment")
continue
}
environments = append(environments, st)
}
return environments, nil
}
func (w *Workspace) Get(ctx context.Context, name string) (*State, error) {
envPath, err := filepath.Abs(w.envPath(name))
if err != nil {
return nil, err
}
if _, err := os.Stat(envPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotExist
}
return nil, err
}
manifest, err := os.ReadFile(path.Join(envPath, manifestFile))
if err != nil {
return nil, err
}
manifest, err = keychain.Decrypt(ctx, manifest)
if err != nil {
return nil, fmt.Errorf("unable to decrypt state: %w", err)
}
var st State
if err := yaml.Unmarshal(manifest, &st); err != nil {
return nil, err
}
st.Path = envPath
st.Plan = path.Join(envPath, planDir)
st.Workspace = w.Path
computed, err := os.ReadFile(path.Join(envPath, stateDir, computedFile))
if err == nil {
st.Computed = string(computed)
}
return &st, nil
}
func (w *Workspace) Save(ctx context.Context, st *State) error {
data, err := yaml.Marshal(st)
if err != nil {
return err
}
manifestPath := path.Join(st.Path, manifestFile)
currentEncrypted, err := os.ReadFile(manifestPath)
if err != nil {
return err
}
currentPlain, err := keychain.Decrypt(ctx, currentEncrypted)
if err != nil {
return fmt.Errorf("unable to decrypt state: %w", err)
}
// Only update the encrypted file if there were changes
if !bytes.Equal(data, currentPlain) {
encrypted, err := keychain.Reencrypt(ctx, manifestPath, data)
if err != nil {
return err
}
if err := os.WriteFile(manifestPath, encrypted, 0600); err != nil {
return err
}
}
if st.Computed != "" {
state := path.Join(st.Path, 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 (w *Workspace) Create(ctx context.Context, name string) (*State, error) {
envPath, err := filepath.Abs(w.envPath(name))
if err != nil {
return nil, err
}
// Environment directory
if err := os.MkdirAll(envPath, 0755); err != nil {
if errors.Is(err, os.ErrExist) {
return nil, ErrExist
}
return nil, err
}
// Plan directory
if err := os.Mkdir(path.Join(envPath, planDir), 0755); err != nil {
if errors.Is(err, os.ErrExist) {
return nil, ErrExist
}
return nil, err
}
manifestPath := path.Join(envPath, manifestFile)
st := &State{
Path: envPath,
Workspace: w.Path,
Plan: path.Join(envPath, planDir),
Name: name,
}
data, err := yaml.Marshal(st)
if err != nil {
return nil, err
}
key, err := keychain.Default(ctx)
if err != nil {
return nil, err
}
encrypted, err := keychain.Encrypt(ctx, manifestPath, data, key)
if err != nil {
return nil, err
}
if err := os.WriteFile(manifestPath, encrypted, 0600); err != nil {
return nil, err
}
err = os.WriteFile(
path.Join(envPath, ".gitignore"),
[]byte("# dagger state\nstate/**\n"),
0600,
)
if err != nil {
return nil, err
}
return st, nil
}

View File

@ -0,0 +1,109 @@
package state
import (
"context"
"os"
"path"
"strings"
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestWorkspace(t *testing.T) {
ctx := context.TODO()
root, err := os.MkdirTemp(os.TempDir(), "dagger-*")
require.NoError(t, err)
// Open should fail since the directory is not initialized
_, err = Open(ctx, root)
require.ErrorIs(t, ErrNotInit, err)
// Init
workspace, err := Init(ctx, root)
require.NoError(t, err)
require.Equal(t, root, workspace.Path)
// Create
st, err := workspace.Create(ctx, "test")
require.NoError(t, err)
require.Equal(t, "test", st.Name)
// Open
workspace, err = Open(ctx, root)
require.NoError(t, err)
require.Equal(t, root, workspace.Path)
// List
envs, err := workspace.List(ctx)
require.NoError(t, err)
require.Len(t, envs, 1)
require.Equal(t, "test", envs[0].Name)
// Get
env, err := workspace.Get(ctx, "test")
require.NoError(t, err)
require.Equal(t, "test", env.Name)
// Save
require.NoError(t, env.SetInput("foo", TextInput("bar")))
require.NoError(t, workspace.Save(ctx, env))
workspace, err = Open(ctx, root)
require.NoError(t, err)
env, err = workspace.Get(ctx, "test")
require.NoError(t, err)
require.Contains(t, env.Inputs, "foo")
}
func TestEncryption(t *testing.T) {
ctx := context.TODO()
readManifest := func(st *State) *State {
data, err := os.ReadFile(path.Join(st.Path, manifestFile))
require.NoError(t, err)
m := State{}
require.NoError(t, yaml.Unmarshal(data, &m))
return &m
}
root, err := os.MkdirTemp(os.TempDir(), "dagger-*")
require.NoError(t, err)
workspace, err := Init(ctx, root)
require.NoError(t, err)
_, err = workspace.Create(ctx, "test")
require.NoError(t, err)
// Set a plaintext input, make sure it is not encrypted
st, err := workspace.Get(ctx, "test")
require.NoError(t, err)
require.NoError(t, st.SetInput("plain", TextInput("plain")))
require.NoError(t, workspace.Save(ctx, st))
o := readManifest(st)
require.Contains(t, o.Inputs, "plain")
require.Equal(t, "plain", string(*o.Inputs["plain"].Text))
// Set a secret input, make sure it's encrypted
st, err = workspace.Get(ctx, "test")
require.NoError(t, err)
require.NoError(t, st.SetInput("secret", SecretInput("secret")))
require.NoError(t, workspace.Save(ctx, st))
o = readManifest(st)
require.Contains(t, o.Inputs, "secret")
secretValue := string(*o.Inputs["secret"].Secret)
require.NotEqual(t, "secret", secretValue)
require.True(t, strings.HasPrefix(secretValue, "ENC["))
// Change another input, make sure our secret didn't change
st, err = workspace.Get(ctx, "test")
require.NoError(t, err)
require.NoError(t, st.SetInput("plain", TextInput("different")))
require.NoError(t, workspace.Save(ctx, st))
o = readManifest(st)
require.Contains(t, o.Inputs, "plain")
require.Equal(t, "different", string(*o.Inputs["plain"].Text))
require.Contains(t, o.Inputs, "secret")
require.Equal(t, secretValue, string(*o.Inputs["secret"].Secret))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -26,15 +26,19 @@ To get started with Cue, we recommend the following resources:
To create a Dagger plan:
1\. Create a new directory anywhere in your git repository.
1\. Initialize a Dagger workspace anywhere in your git repository.
For example: `mkdir staging`.
`dagger init`.
2\. Create a new file with the *.cue* extension, and open it with any text editor or IDE.
2\. Create a new environment.
For example: `staging.cue`.
For example: `dagger new staging`.
3\. Describe each relay in your plan as a field in the cue configuration.
3\. Create a new file with the *.cue* extension in `.dagger/env/staging/plan`, and open it with any text editor or IDE.
For example: `.dagger/env/staging/plan/staging.cue`.
4\. Describe each relay in your plan as a field in the cue configuration.
For example:
@ -67,9 +71,9 @@ For more inspiration, see these examples:
* [Add HTTP monitoring to your application](https://github.com/dagger/dagger/blob/main/examples/README.md#add-http-monitoring-to-your-application)
* [Deploy an application to your Kubernetes cluster](https://github.com/dagger/dagger/blob/main/examples/README.md#deploy-an-application-to-your-kubernetes-cluster)
4\. Extend your plan with relay definitions from [Dagger Universe](../stdlib), an encyclopedia of cue packages curated by the Dagger community.
5\. Extend your plan with relay definitions from [Dagger Universe](../stdlib), an encyclopedia of cue packages curated by the Dagger community.
5\. If you can't find the relay you need in the Universe, you can simply create your own.
6\. If you can't find the relay you need in the Universe, you can simply create your own.
For example:

4
go.mod
View File

@ -3,13 +3,13 @@ module dagger.io/go
go 1.16
require (
cuelang.org/go v0.4.0-rc.1
cuelang.org/go v0.4.0-beta.1
filippo.io/age v1.0.0-rc.1
github.com/HdrHistogram/hdrhistogram-go v1.1.0 // indirect
github.com/KromDaniel/jonson v0.0.0-20180630143114-d2f9c3c389db
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

9
go.sum
View File

@ -44,12 +44,13 @@ contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrL
contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
cuelang.org/go v0.4.0-rc.1 h1:X8fsqVhLCvXFhsWMGbI8rjTal45YOgt+ko+m7rOCySM=
cuelang.org/go v0.4.0-rc.1/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs=
cuelang.org/go v0.4.0-beta.1 h1:/YjeAmymfNdTLSA3jHXNrj8Q+5Zq9by7qNOssqUBM+c=
cuelang.org/go v0.4.0-beta.1/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/age v1.0.0-beta7 h1:RZiSK+N3KL2UwT82xiCavjYw8jJHzWMEUYePAukTpk0=
filippo.io/age v1.0.0-beta7/go.mod h1:chAuTrTb0FTTmKtvs6fQTGhYTvH9AigjN1uEUsvLdZ0=
filippo.io/age v1.0.0-rc.1 h1:jQ+dz16Xxx3W/WY+YS0J96nVAAidLHO3kfQe0eOmKgI=
filippo.io/age v1.0.0-rc.1/go.mod h1:Vvd9IlwNo4Au31iqNZeZVnYtGcOf/wT4mtvZQ2ODlSk=
filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
@ -899,7 +900,6 @@ github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7A
github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+yDFh9SZXUTvspXTjbFXgZGP/UvhU1S65A4A=
github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
@ -1084,6 +1084,7 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=

View File

@ -4,55 +4,45 @@ setup() {
common_setup
}
@test "dagger list" {
run "$DAGGER" list
@test "dagger init" {
run "$DAGGER" init
assert_success
assert_output ""
"$DAGGER" new --plan-dir "$TESTDIR"/cli/simple simple
run "$DAGGER" list
assert_success
assert_output --partial "simple"
refute_output
run "$DAGGER" init
assert_failure
}
@test "dagger new --plan-dir" {
run "$DAGGER" list
assert_success
assert_output ""
"$DAGGER" new --plan-dir "$TESTDIR"/cli/simple simple
# duplicate name
run "$DAGGER" new --plan-dir "$TESTDIR"/cli/simple simple
@test "dagger new" {
run "$DAGGER" new "test"
assert_failure
# verify the plan works
"$DAGGER" up -e "simple"
# verify we have the right plan
run "$DAGGER" query -f cue -e "simple" -c -f json
run "$DAGGER" init
assert_success
assert_output --partial '{
"bar": "another value",
"computed": "test",
"foo": "value"
}'
}
@test "dagger new --plan-git" {
"$DAGGER" new --plan-git https://github.com/samalba/dagger-test.git simple
"$DAGGER" up -e "simple"
run "$DAGGER" query -f cue -e "simple" -c
run "$DAGGER" list
assert_success
assert_output --partial '{
foo: "value"
bar: "another value"
}'
refute_output
run "$DAGGER" new "test"
assert_success
run "$DAGGER" list
assert_success
assert_output --partial "test"
run "$DAGGER" new "test"
assert_failure
}
@test "dagger query" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/simple simple
"$DAGGER" init
dagger_new_with_plan simple "$TESTDIR"/cli/simple
run "$DAGGER" query -l error -e "simple"
assert_success
assert_output '{
@ -93,24 +83,10 @@ setup() {
}'
}
@test "dagger plan" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/simple simple
# plan dir
"$DAGGER" -e "simple" plan dir "$TESTDIR"/cli/simple
run "$DAGGER" -e "simple" query
assert_success
assert_output --partial '"foo": "value"'
# plan git
"$DAGGER" -e "simple" plan git https://github.com/samalba/dagger-test.git
run "$DAGGER" -e "simple" query
assert_success
assert_output --partial '"foo": "value"'
}
@test "dagger input text" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/input/simple "input"
"$DAGGER" init
dagger_new_with_plan input "$TESTDIR"/cli/input/simple
# simple input
"$DAGGER" input -e "input" text "input" "my input"
@ -176,7 +152,9 @@ setup() {
}
@test "dagger input json" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/input/simple "input"
"$DAGGER" init
dagger_new_with_plan input "$TESTDIR"/cli/input/simple
# simple json
"$DAGGER" input -e "input" json "structured" '{"a": "foo", "b": 42}'
@ -214,7 +192,9 @@ setup() {
}
@test "dagger input yaml" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/input/simple "input"
"$DAGGER" init
dagger_new_with_plan input "$TESTDIR"/cli/input/simple
# simple yaml
"$DAGGER" input -e "input" yaml "structured" '{"a": "foo", "b": 42}'
@ -252,10 +232,17 @@ setup() {
}
@test "dagger input dir" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/input/artifact "input"
"$DAGGER" init
# input dir
"$DAGGER" input -e "input" dir "source" "$TESTDIR"/cli/input/artifact/testdata
dagger_new_with_plan input "$TESTDIR"/cli/input/artifact
# input dir outside the workspace
run "$DAGGER" input -e "input" dir "source" /tmp
assert_failure
# input dir inside the workspace
cp -R "$TESTDIR"/cli/input/artifact/testdata/ "$DAGGER_WORKSPACE"/testdata
"$DAGGER" input -e "input" dir "source" "$DAGGER_WORKSPACE"/testdata
"$DAGGER" up -e "input"
run "$DAGGER" -l error query -e "input"
assert_success
@ -276,7 +263,9 @@ setup() {
}
@test "dagger input git" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/input/artifact "input"
"$DAGGER" init
dagger_new_with_plan input "$TESTDIR"/cli/input/artifact
# input git
"$DAGGER" input -e "input" git "source" https://github.com/samalba/dagger-test-simple.git
@ -296,11 +285,3 @@ setup() {
"foo": "bar"
}'
}
@test "dagger input scan" {
"$DAGGER" new --plan-dir "$TESTDIR"/cli/input/scan "scan"
# TODO "scan" option isn't implemented
run "$DAGGER" input scan -e "input"
assert_success
}

View File

@ -7,13 +7,13 @@ setup() {
@test "example: react" {
skip_unless_secrets_available "$TESTDIR"/examples/react/inputs.yaml
"$DAGGER" new --plan-dir "$TESTDIR"/../examples/react react
"$DAGGER" init
dagger_new_with_plan react "$TESTDIR"/../examples/react
sops -d "$TESTDIR"/examples/react/inputs.yaml | "$DAGGER" -e "react" input yaml "" -f -
"$DAGGER" up -e "react"
# curl the URL we just deployed to check if it worked
deployUrl=$("$DAGGER" query -l error -f text -e "react" www.deployUrl)
echo "=>$deployUrl<="
run curl -sS "$deployUrl"
assert_success
assert_output --partial "Todo App"

View File

@ -10,8 +10,18 @@ common_setup() {
DAGGER_LOG_FORMAT="pretty"
export DAGGER_LOG_FORMAT
DAGGER_STORE="$(mktemp -d -t dagger-store-XXXXXX)"
export DAGGER_STORE
DAGGER_WORKSPACE="$(mktemp -d -t dagger-workspace-XXXXXX)"
export DAGGER_WORKSPACE
}
dagger_new_with_plan() {
local name="$1"
local sourcePlan="$2"
local targetPlan="$DAGGER_WORKSPACE"/.dagger/env/"$name"/plan
"$DAGGER" new "$name"
rmdir "$targetPlan"
ln -s "$sourcePlan" "$targetPlan"
}
skip_unless_secrets_available() {

View File

@ -91,8 +91,11 @@ setup() {
@test "stdlib: terraform" {
skip_unless_secrets_available "$TESTDIR"/stdlib/aws/inputs.yaml
"$DAGGER" new --plan-dir "$TESTDIR"/stdlib/terraform/s3 terraform
"$DAGGER" -e terraform input dir TestData "$TESTDIR"/stdlib/terraform/s3/testdata
"$DAGGER" init
dagger_new_with_plan terraform "$TESTDIR"/stdlib/terraform/s3
cp -R "$TESTDIR"/stdlib/terraform/s3/testdata "$DAGGER_WORKSPACE"/testdata
"$DAGGER" -e terraform input dir TestData "$DAGGER_WORKSPACE"/testdata
sops -d "$TESTDIR"/stdlib/aws/inputs.yaml | "$DAGGER" -e "terraform" input yaml "" -f -
# it must fail because of a missing var