Remove state (State + Project)
Signed-off-by: Joel Longtine <joel@dagger.io>
This commit is contained in:
parent
034cd74ed0
commit
4e1b6173be
@ -12,78 +12,8 @@ import (
|
|||||||
"go.dagger.io/dagger/client"
|
"go.dagger.io/dagger/client"
|
||||||
"go.dagger.io/dagger/compiler"
|
"go.dagger.io/dagger/compiler"
|
||||||
"go.dagger.io/dagger/plancontext"
|
"go.dagger.io/dagger/plancontext"
|
||||||
"go.dagger.io/dagger/state"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func CurrentProject(ctx context.Context) *state.Project {
|
|
||||||
lg := log.Ctx(ctx)
|
|
||||||
|
|
||||||
if projectPath := viper.GetString("project"); projectPath != "" {
|
|
||||||
project, err := state.Open(ctx, projectPath)
|
|
||||||
if err != nil {
|
|
||||||
lg.
|
|
||||||
Fatal().
|
|
||||||
Err(err).
|
|
||||||
Str("path", projectPath).
|
|
||||||
Msg("failed to open project")
|
|
||||||
}
|
|
||||||
return project
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := state.Current(ctx)
|
|
||||||
if err != nil {
|
|
||||||
lg.
|
|
||||||
Fatal().
|
|
||||||
Err(err).
|
|
||||||
Msg("failed to determine current project")
|
|
||||||
}
|
|
||||||
return project
|
|
||||||
}
|
|
||||||
|
|
||||||
func CurrentEnvironmentState(ctx context.Context, project *state.Project) *state.State {
|
|
||||||
lg := log.Ctx(ctx)
|
|
||||||
|
|
||||||
environmentName := viper.GetString("environment")
|
|
||||||
if environmentName != "" {
|
|
||||||
st, err := project.Get(ctx, environmentName)
|
|
||||||
if err != nil {
|
|
||||||
lg.
|
|
||||||
Fatal().
|
|
||||||
Err(err).
|
|
||||||
Msg("failed to load environment")
|
|
||||||
}
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
|
|
||||||
environments, err := project.List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
lg.
|
|
||||||
Fatal().
|
|
||||||
Err(err).
|
|
||||||
Msg("failed to list environments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(environments) == 0 {
|
|
||||||
lg.
|
|
||||||
Fatal().
|
|
||||||
Msg("no environments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(environments) > 1 {
|
|
||||||
envNames := []string{}
|
|
||||||
for _, e := range environments {
|
|
||||||
envNames = append(envNames, e.Name)
|
|
||||||
}
|
|
||||||
lg.
|
|
||||||
Fatal().
|
|
||||||
Err(err).
|
|
||||||
Strs("environments", envNames).
|
|
||||||
Msg("multiple environments available in the project, select one with `--environment`")
|
|
||||||
}
|
|
||||||
|
|
||||||
return environments[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatValue returns the String representation of the cue value
|
// FormatValue returns the String representation of the cue value
|
||||||
func FormatValue(val *compiler.Value) string {
|
func FormatValue(val *compiler.Value) string {
|
||||||
switch {
|
switch {
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.dagger.io/dagger/state"
|
|
||||||
"go.dagger.io/dagger/telemetry"
|
"go.dagger.io/dagger/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,34 +31,6 @@ func commandName(cmd *cobra.Command) string {
|
|||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackProjectCommand is like TrackCommand but includes project and
|
|
||||||
// optionally environment metadata.
|
|
||||||
func TrackProjectCommand(ctx context.Context, cmd *cobra.Command, w *state.Project, env *state.State, props ...*telemetry.Property) chan struct{} {
|
|
||||||
props = append([]*telemetry.Property{
|
|
||||||
{
|
|
||||||
// Hash the repository URL for privacy
|
|
||||||
Name: "git_repository_hash",
|
|
||||||
Value: hash(gitRepoURL(w.Path)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// The project path might contain the username (e.g. /home/user/project), so we hash it for privacy.
|
|
||||||
Name: "project_path_hash",
|
|
||||||
Value: hash(w.Path),
|
|
||||||
},
|
|
||||||
}, props...)
|
|
||||||
|
|
||||||
if env != nil {
|
|
||||||
props = append([]*telemetry.Property{
|
|
||||||
{
|
|
||||||
Name: "environment_name",
|
|
||||||
Value: env.Name,
|
|
||||||
},
|
|
||||||
}, props...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return TrackCommand(ctx, cmd, props...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hash returns the sha256 digest of the string
|
// hash returns the sha256 digest of the string
|
||||||
func hash(s string) string {
|
func hash(s string) string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
|
||||||
|
@ -5,9 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.dagger.io/dagger/cmd/dagger/cmd/common"
|
|
||||||
"go.dagger.io/dagger/cmd/dagger/logger"
|
"go.dagger.io/dagger/cmd/dagger/logger"
|
||||||
"go.dagger.io/dagger/state"
|
"go.dagger.io/dagger/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var initCmd = &cobra.Command{
|
var initCmd = &cobra.Command{
|
||||||
@ -37,12 +36,13 @@ var initCmd = &cobra.Command{
|
|||||||
dir = cwd
|
dir = cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
project, err := state.Init(ctx, dir)
|
err := pkg.CueModInit(ctx, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Fatal().Err(err).Msg("failed to initialize project")
|
lg.Fatal().Err(err).Msg("failed to initialize project")
|
||||||
}
|
}
|
||||||
|
|
||||||
<-common.TrackProjectCommand(ctx, cmd, project, nil)
|
// TODO: Add telemtry for init
|
||||||
|
// <-common.TrackProjectCommand(ctx, cmd, project, nil)
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"go.dagger.io/dagger/cmd/dagger/logger"
|
"go.dagger.io/dagger/cmd/dagger/logger"
|
||||||
"go.dagger.io/dagger/mod"
|
"go.dagger.io/dagger/mod"
|
||||||
"go.dagger.io/dagger/pkg"
|
"go.dagger.io/dagger/pkg"
|
||||||
"go.dagger.io/dagger/state"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var getCmd = &cobra.Command{
|
var getCmd = &cobra.Command{
|
||||||
@ -28,10 +27,10 @@ var getCmd = &cobra.Command{
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
cueModPath := pkg.GetCueModParent()
|
cueModPath := pkg.GetCueModParent()
|
||||||
// err = pkg.CueModInit(ctx, cueModPath)
|
err = pkg.CueModInit(ctx, cueModPath)
|
||||||
_, err = state.Init(ctx, cueModPath)
|
if err != nil {
|
||||||
if err != nil && err != state.ErrAlreadyInit {
|
|
||||||
lg.Fatal().Err(err).Msg("failed to initialize cue.mod")
|
lg.Fatal().Err(err).Msg("failed to initialize cue.mod")
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var update = viper.GetBool("update")
|
var update = viper.GetBool("update")
|
||||||
|
@ -59,7 +59,7 @@ func Vendor(ctx context.Context, p string) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// ensure cue module is initialized
|
// ensure cue module is initialized
|
||||||
if err := cueModInit(ctx, p); err != nil {
|
if err := CueModInit(ctx, p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ func GetCueModParent() string {
|
|||||||
return parentDir
|
return parentDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func cueModInit(ctx context.Context, parentDir string) error {
|
func CueModInit(ctx context.Context, parentDir string) error {
|
||||||
lg := log.Ctx(ctx)
|
lg := log.Ctx(ctx)
|
||||||
|
|
||||||
modDir := path.Join(parentDir, "cue.mod")
|
modDir := path.Join(parentDir, "cue.mod")
|
||||||
|
300
state/input.go
300
state/input.go
@ -1,300 +0,0 @@
|
|||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"cuelang.org/go/cue"
|
|
||||||
|
|
||||||
"go.dagger.io/dagger/compiler"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 struct {
|
|
||||||
Dir *dirInput `yaml:"dir,omitempty"`
|
|
||||||
Git *gitInput `yaml:"git,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"`
|
|
||||||
Bool *boolInput `yaml:"bool,omitempty"`
|
|
||||||
Socket *socketInput `yaml:"socket,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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)
|
|
||||||
case i.Bool != nil:
|
|
||||||
return i.Bool.Compile(state)
|
|
||||||
case i.Socket != nil:
|
|
||||||
return i.Socket.Compile(state)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("input has not been set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An input artifact loaded from a local directory
|
|
||||||
func DirInput(path string, include []string, exclude []string) Input {
|
|
||||||
return Input{
|
|
||||||
Dir: &dirInput{
|
|
||||||
Path: path,
|
|
||||||
Include: include,
|
|
||||||
Exclude: exclude,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type dirInput struct {
|
|
||||||
Path string `yaml:"path,omitempty"`
|
|
||||||
Include []string `yaml:"include,omitempty"`
|
|
||||||
Exclude []string `yaml:"exclude,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// in Cue because `null` is not a `[...string]`
|
|
||||||
includeLLB := []byte("[]")
|
|
||||||
if len(dir.Include) > 0 {
|
|
||||||
var err error
|
|
||||||
includeLLB, err = json.Marshal(dir.Include)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
excludeLLB := []byte("[]")
|
|
||||||
if len(dir.Exclude) > 0 {
|
|
||||||
var err error
|
|
||||||
excludeLLB, err = json.Marshal(dir.Exclude)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := dir.Path
|
|
||||||
if !filepath.IsAbs(p) {
|
|
||||||
p = filepath.Clean(path.Join(state.Project, dir.Path))
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(p, state.Project) {
|
|
||||||
return nil, fmt.Errorf("%q is outside the project", dir.Path)
|
|
||||||
}
|
|
||||||
// Check that directory exists
|
|
||||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("%q dir doesn't exist", dir.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
dirPath, err := json.Marshal(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Context.LocalDirs.Add(p)
|
|
||||||
|
|
||||||
llb := fmt.Sprintf(
|
|
||||||
`#up: [{do: "local", dir: %s, include: %s, exclude: %s}]`,
|
|
||||||
dirPath,
|
|
||||||
includeLLB,
|
|
||||||
excludeLLB,
|
|
||||||
)
|
|
||||||
return compiler.Compile("", llb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// An input artifact loaded from a git repository
|
|
||||||
type gitInput struct {
|
|
||||||
Remote string `yaml:"remote,omitempty"`
|
|
||||||
Ref string `yaml:"ref,omitempty"`
|
|
||||||
Dir string `yaml:"dir,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GitInput(remote, ref, dir string) Input {
|
|
||||||
return Input{
|
|
||||||
Git: &gitInput{
|
|
||||||
Remote: remote,
|
|
||||||
Ref: ref,
|
|
||||||
Dir: dir,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (git gitInput) Compile(_ *State) (*compiler.Value, error) {
|
|
||||||
ref := "HEAD"
|
|
||||||
if git.Ref != "" {
|
|
||||||
ref = git.Ref
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := ""
|
|
||||||
if git.Dir != "" {
|
|
||||||
dir = fmt.Sprintf(`,{do:"subdir", dir:"%s"}`, git.Dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiler.Compile("", fmt.Sprintf(
|
|
||||||
`#up: [{do:"fetch-git", remote:"%s", ref:"%s"}%s]`,
|
|
||||||
git.Remote,
|
|
||||||
ref,
|
|
||||||
dir,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// An input value encoded as text
|
|
||||||
func TextInput(data string) Input {
|
|
||||||
i := textInput(data)
|
|
||||||
return Input{
|
|
||||||
Text: &i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type textInput string
|
|
||||||
|
|
||||||
func (i textInput) Compile(_ *State) (*compiler.Value, error) {
|
|
||||||
return compiler.Compile("", fmt.Sprintf("%q", i))
|
|
||||||
}
|
|
||||||
|
|
||||||
// A secret input value
|
|
||||||
func SecretInput(data string) Input {
|
|
||||||
i := secretInput(data)
|
|
||||||
return Input{
|
|
||||||
Secret: &i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type secretInput string
|
|
||||||
|
|
||||||
func (i secretInput) Compile(st *State) (*compiler.Value, error) {
|
|
||||||
secret := st.Context.Secrets.New(i.PlainText())
|
|
||||||
return secret.MarshalCUE(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i secretInput) PlainText() string {
|
|
||||||
return string(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// An input value encoded as Bool
|
|
||||||
func BoolInput(data string) Input {
|
|
||||||
i := boolInput(data)
|
|
||||||
return Input{
|
|
||||||
Bool: &i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type boolInput string
|
|
||||||
|
|
||||||
func (i boolInput) Compile(_ *State) (*compiler.Value, error) {
|
|
||||||
s := map[boolInput]struct{}{
|
|
||||||
"true": {},
|
|
||||||
"false": {},
|
|
||||||
}
|
|
||||||
if _, ok := s[i]; ok {
|
|
||||||
return compiler.DecodeJSON("", []byte(i))
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%q is not a valid boolean: <true|false>", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// An input value encoded as JSON
|
|
||||||
func JSONInput(data string) Input {
|
|
||||||
i := jsonInput(data)
|
|
||||||
return Input{
|
|
||||||
JSON: &i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsonInput string
|
|
||||||
|
|
||||||
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{
|
|
||||||
YAML: &i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type yamlInput string
|
|
||||||
|
|
||||||
func (i yamlInput) Compile(_ *State) (*compiler.Value, error) {
|
|
||||||
return compiler.DecodeYAML("", []byte(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
func FileInput(data string) Input {
|
|
||||||
return Input{
|
|
||||||
File: &fileInput{
|
|
||||||
Path: data,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fileInput struct {
|
|
||||||
Path string `yaml:"path,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i fileInput) Compile(_ *State) (*compiler.Value, error) {
|
|
||||||
data, err := ioutil.ReadFile(i.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
value := compiler.NewValue()
|
|
||||||
if err := value.FillPath(cue.MakePath(), data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// A socket input value
|
|
||||||
func SocketInput(data, socketType string) Input {
|
|
||||||
i := socketInput{}
|
|
||||||
|
|
||||||
switch socketType {
|
|
||||||
case "npipe":
|
|
||||||
i.Npipe = data
|
|
||||||
case "unix":
|
|
||||||
i.Unix = data
|
|
||||||
}
|
|
||||||
|
|
||||||
return Input{
|
|
||||||
Socket: &i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type socketInput struct {
|
|
||||||
Unix string `json:"unix,omitempty" yaml:"unix,omitempty"`
|
|
||||||
Npipe string `json:"npipe,omitempty" yaml:"npipe,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i socketInput) Compile(st *State) (*compiler.Value, error) {
|
|
||||||
service := st.Context.Services.New(i.Unix, i.Npipe)
|
|
||||||
return service.MarshalCUE(), nil
|
|
||||||
}
|
|
354
state/project.go
354
state/project.go
@ -1,354 +0,0 @@
|
|||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"go.dagger.io/dagger/keychain"
|
|
||||||
"go.dagger.io/dagger/pkg"
|
|
||||||
"go.dagger.io/dagger/plancontext"
|
|
||||||
"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 Project struct {
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Init(ctx context.Context, dir string) (*Project, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pkg.Vendor(ctx, root); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Project{
|
|
||||||
Path: root,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Open(ctx context.Context, dir string) (*Project, 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 &Project{
|
|
||||||
Path: root,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Current(ctx context.Context) (*Project, 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 *Project) envPath(name string) string {
|
|
||||||
return path.Join(w.Path, daggerDir, envDir, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Project) 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 {
|
|
||||||
// If the environment doesn't exist (e.g. no values.yaml, skip silently)
|
|
||||||
if !errors.Is(err, ErrNotExist) {
|
|
||||||
log.
|
|
||||||
Ctx(ctx).
|
|
||||||
Err(err).
|
|
||||||
Str("name", f.Name()).
|
|
||||||
Msg("failed to load environment")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
environments = append(environments, st)
|
|
||||||
}
|
|
||||||
|
|
||||||
return environments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Project) 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 {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, ErrNotExist
|
|
||||||
}
|
|
||||||
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.Context = plancontext.New()
|
|
||||||
if platform := st.Platform; platform != "" {
|
|
||||||
if err := st.Context.Platform.Set(platform); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
st.Path = envPath
|
|
||||||
// FIXME: Backward compat: Support for old-style `.dagger/env/<name>/plan`
|
|
||||||
if st.Plan.Module == "" {
|
|
||||||
planPath := path.Join(envPath, planDir)
|
|
||||||
if _, err := os.Stat(planPath); err == nil {
|
|
||||||
planRelPath, err := filepath.Rel(w.Path, planPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
st.Plan.Module = planRelPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
st.Project = w.Path
|
|
||||||
|
|
||||||
computed, err := os.ReadFile(path.Join(envPath, stateDir, computedFile))
|
|
||||||
if err == nil {
|
|
||||||
st.Computed = string(computed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &st, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Project) 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 *Project) Create(ctx context.Context, name string, plan Plan, platform string) (*State, error) {
|
|
||||||
if _, err := w.Get(ctx, name); err == nil {
|
|
||||||
return nil, ErrExist
|
|
||||||
}
|
|
||||||
|
|
||||||
pkg, err := w.cleanPackageName(ctx, plan.Package)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
envPath, err := filepath.Abs(w.envPath(name))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment directory
|
|
||||||
if err := os.MkdirAll(envPath, 0755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
manifestPath := path.Join(envPath, manifestFile)
|
|
||||||
|
|
||||||
st := &State{
|
|
||||||
Context: plancontext.New(),
|
|
||||||
|
|
||||||
Path: envPath,
|
|
||||||
Project: w.Path,
|
|
||||||
Plan: Plan{
|
|
||||||
Package: pkg,
|
|
||||||
},
|
|
||||||
Name: name,
|
|
||||||
Platform: platform,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Project) cleanPackageName(ctx context.Context, pkg string) (string, error) {
|
|
||||||
lg := log.
|
|
||||||
Ctx(ctx).
|
|
||||||
With().
|
|
||||||
Str("package", pkg).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
if pkg == "" {
|
|
||||||
return pkg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the package is not a path, then it must be a domain (e.g. foo.bar/mypackage)
|
|
||||||
if _, err := os.Stat(pkg); err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the domain is in the correct form
|
|
||||||
if !strings.Contains(pkg, ".") || !strings.Contains(pkg, "/") {
|
|
||||||
return "", fmt.Errorf("invalid package %q", pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := filepath.Abs(pkg)
|
|
||||||
if err != nil {
|
|
||||||
lg.Error().Err(err).Msg("unable to resolve path")
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(p, w.Path) {
|
|
||||||
lg.Fatal().Err(err).Msg("package is outside the project")
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err = filepath.Rel(w.Path, p)
|
|
||||||
if err != nil {
|
|
||||||
lg.Fatal().Err(err).Msg("unable to resolve path")
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(p, ".") {
|
|
||||||
p = "./" + p
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go.dagger.io/dagger/keychain"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestProject(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
keychain.EnsureDefaultKey(ctx)
|
|
||||||
|
|
||||||
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
|
|
||||||
project, err := Init(ctx, root)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, root, project.Path)
|
|
||||||
|
|
||||||
// Create
|
|
||||||
st, err := project.Create(ctx, "test", Plan{
|
|
||||||
Module: ".",
|
|
||||||
}, "linux/amd64")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "test", st.Name)
|
|
||||||
require.Equal(t, "linux/amd64", st.Platform)
|
|
||||||
|
|
||||||
// Open
|
|
||||||
project, err = Open(ctx, root)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, root, project.Path)
|
|
||||||
|
|
||||||
// List
|
|
||||||
envs, err := project.List(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, envs, 1)
|
|
||||||
require.Equal(t, "test", envs[0].Name)
|
|
||||||
|
|
||||||
// Get
|
|
||||||
env, err := project.Get(ctx, "test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "test", env.Name)
|
|
||||||
require.Equal(t, "linux/amd64", env.Platform)
|
|
||||||
|
|
||||||
// Save
|
|
||||||
require.NoError(t, env.SetInput("foo", TextInput("bar")))
|
|
||||||
require.NoError(t, project.Save(ctx, env))
|
|
||||||
project, err = Open(ctx, root)
|
|
||||||
require.NoError(t, err)
|
|
||||||
env, err = project.Get(ctx, "test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Contains(t, env.Inputs, "foo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncryption(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
keychain.EnsureDefaultKey(ctx)
|
|
||||||
|
|
||||||
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)
|
|
||||||
project, err := Init(ctx, root)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = project.Create(ctx, "test", Plan{
|
|
||||||
Module: ".",
|
|
||||||
}, "linux/amd64")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Set a plaintext input, make sure it is not encrypted
|
|
||||||
st, err := project.Get(ctx, "test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, st.SetInput("plain", TextInput("plain")))
|
|
||||||
require.NoError(t, project.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 = project.Get(ctx, "test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, st.SetInput("secret", SecretInput("secret")))
|
|
||||||
require.NoError(t, project.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 = project.Get(ctx, "test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, st.SetInput("plain", TextInput("different")))
|
|
||||||
require.NoError(t, project.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))
|
|
||||||
}
|
|
112
state/state.go
112
state/state.go
@ -1,112 +0,0 @@
|
|||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"cuelang.org/go/cue"
|
|
||||||
"go.dagger.io/dagger/compiler"
|
|
||||||
"go.dagger.io/dagger/pkg"
|
|
||||||
"go.dagger.io/dagger/plancontext"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Contents of an environment serialized to a file
|
|
||||||
type State struct {
|
|
||||||
// Plan Context.
|
|
||||||
// FIXME: this is used as a bridge and is temporary.
|
|
||||||
Context *plancontext.Context `yaml:"-"`
|
|
||||||
|
|
||||||
// State path
|
|
||||||
Path string `yaml:"-"`
|
|
||||||
|
|
||||||
// Project path
|
|
||||||
Project string `yaml:"-"`
|
|
||||||
|
|
||||||
// Plan
|
|
||||||
Plan Plan `yaml:"plan,omitempty"`
|
|
||||||
|
|
||||||
// Human-friendly environment name.
|
|
||||||
// A environment may have more than one name.
|
|
||||||
// FIXME: store multiple names?
|
|
||||||
Name string `yaml:"name,omitempty"`
|
|
||||||
|
|
||||||
// Platform execution
|
|
||||||
Platform string `yaml:"platform,omitempty"`
|
|
||||||
|
|
||||||
// User Inputs
|
|
||||||
Inputs map[string]Input `yaml:"inputs,omitempty"`
|
|
||||||
|
|
||||||
// Computed values
|
|
||||||
Computed string `yaml:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cue module containing the environment plan
|
|
||||||
func (s *State) CompilePlan(ctx context.Context) (*compiler.Value, error) {
|
|
||||||
w := s.Project
|
|
||||||
// FIXME: backward compatibility
|
|
||||||
if planModule := s.Plan.Module; planModule != "" {
|
|
||||||
w = path.Join(w, planModule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: universe vendoring
|
|
||||||
// This is already done on `dagger init` and shouldn't be done here too.
|
|
||||||
// However:
|
|
||||||
// 1) As of right now, there's no way to update universe through the
|
|
||||||
// CLI, so we are lazily updating on `dagger up` using the embedded `universe`
|
|
||||||
// 2) For backward compatibility: if the project was `dagger
|
|
||||||
// init`-ed before we added support for vendoring universe, it might not
|
|
||||||
// contain a `cue.mod`.
|
|
||||||
if err := pkg.Vendor(ctx, w); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var args []string
|
|
||||||
if pkg := s.Plan.Package; pkg != "" {
|
|
||||||
args = append(args, pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiler.Build(w, nil, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) CompileInputs() (*compiler.Value, error) {
|
|
||||||
v := compiler.NewValue()
|
|
||||||
|
|
||||||
// Prepare inputs
|
|
||||||
for key, input := range s.Inputs {
|
|
||||||
i, err := input.Compile(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "" {
|
|
||||||
err = v.FillPath(cue.MakePath(), i)
|
|
||||||
} else {
|
|
||||||
err = v.FillPath(cue.ParsePath(key), i)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Plan struct {
|
|
||||||
Module string `yaml:"module,omitempty"`
|
|
||||||
Package string `yaml:"package,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
package state
|
|
Reference in New Issue
Block a user