diff --git a/go.mod b/go.mod index 326eb10c..3e569ff5 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.2 github.com/rs/zerolog v1.26.0 + github.com/sergi/go-diff v1.1.0 // indirect github.com/spf13/cobra v1.2.1 github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.7.0 diff --git a/pkg/dagger.io/dagger/engine/transformsecret.cue b/pkg/dagger.io/dagger/engine/transformsecret.cue new file mode 100644 index 00000000..35da8b82 --- /dev/null +++ b/pkg/dagger.io/dagger/engine/transformsecret.cue @@ -0,0 +1,18 @@ +package engine + +// Securely apply a CUE transformation on the contents of a secret +#TransformSecret: { + $dagger: task: _name: "TransformSecret" + // The original secret + input: #Secret + // A new secret or (map of secrets) with the transformation applied + output: #Secret | {[string]: output} + // Transformation function + #function: { + // Full contents of the input secret (only available to the function) + input: string + _functionOutput: string | {[string]: _functionOutput} + // New contents of the output secret (must provided by the caller) + output: _functionOutput + } +} diff --git a/plan/task/transformsecret.go b/plan/task/transformsecret.go new file mode 100644 index 00000000..c956e2e8 --- /dev/null +++ b/plan/task/transformsecret.go @@ -0,0 +1,68 @@ +package task + +import ( + "context" + "errors" + "strings" + + "cuelang.org/go/cue" + "github.com/rs/zerolog/log" + "github.com/sergi/go-diff/diffmatchpatch" + "go.dagger.io/dagger/compiler" + "go.dagger.io/dagger/plancontext" + "go.dagger.io/dagger/solver" +) + +func init() { + Register("TransformSecret", func() Task { return &transformSecretTask{} }) +} + +type transformSecretTask struct { +} + +func (c *transformSecretTask) Run(ctx context.Context, pctx *plancontext.Context, _ solver.Solver, v *compiler.Value) (*compiler.Value, error) { + lg := log.Ctx(ctx) + lg.Debug().Msg("transforming secret") + + input := v.Lookup("input") + + inputSecret, err := pctx.Secrets.FromValue(input) + if err != nil { + return nil, err + } + + function := v.Lookup("#function") + inputSecretPlaintext := inputSecret.PlainText() + err = function.FillPath(cue.ParsePath("input"), inputSecretPlaintext) + if err != nil { + dmp := diffmatchpatch.New() + errStr := err.Error() + diffs := dmp.DiffMain(inputSecretPlaintext, err.Error(), false) + for _, diff := range diffs { + if diff.Type == diffmatchpatch.DiffEqual { + // diffText := strings.ReplaceAll(diff.Text, ":", "") // colons are tricky. Yaml keys end with them but if a secret contained one that got replaced, the secret wouldn't get redacted + errStr = strings.ReplaceAll(errStr, diff.Text, "***") + } + } + + return nil, errors.New(errStr) + } + + output := compiler.NewValue() + // users could yaml.Unmarshal(input) and return a map + // or yaml.Unmarshal(input).someKey and return a string + // walk will ensure we convert every leaf + functionPathSelectors := function.Path().Selectors() + function.Lookup("output").Walk(nil, func(v *compiler.Value) { + if v.Kind() == cue.StringKind { + plaintext, _ := v.String() + secret := pctx.Secrets.New(plaintext) + newLeafSelectors := v.Path().Selectors()[len(functionPathSelectors):] + newLeafSelectors = append(newLeafSelectors, cue.Str("contents")) + newLeafPath := cue.MakePath(newLeafSelectors...) + output.FillPath(newLeafPath, secret.MarshalCUE()) + } + }) + + return output, nil +} diff --git a/tests/tasks/build/build_auth.cue b/tests/tasks/build/build_auth.cue index 79d2fa05..8b34138a 100644 --- a/tests/tasks/build/build_auth.cue +++ b/tests/tasks/build/build_auth.cue @@ -2,26 +2,38 @@ package testing import ( "dagger.io/dagger/engine" + "encoding/yaml" ) engine.#Plan & { inputs: { directories: testdata: path: "./testdata" - secrets: dockerHubToken: command: { + secrets: sops: command: { name: "sops" - args: ["exec-env", "../../secrets_sops.yaml", "echo $DOCKERHUB_TOKEN"] + args: ["-d", "../../secrets_sops.yaml"] } } - actions: build: engine.#Build & { - source: inputs.directories.testdata.contents - auth: [{ - target: "daggerio/ci-test:private-pull" - username: "daggertest" - secret: inputs.secrets.dockerHubToken.contents - }] - dockerfile: contents: """ - FROM daggerio/ci-test:private-pull@sha256:c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060 - """ + actions: { + dockerHubToken: engine.#TransformSecret & { + input: inputs.secrets.sops.contents + #function: { + input: _ + output: yaml.Unmarshal(input) + } + } + + build: engine.#Build & { + source: inputs.directories.testdata.contents + auth: [{ + target: "daggerio/ci-test:private-pull" + username: "daggertest" + + secret: dockerHubToken.output.DOCKERHUB_TOKEN.contents + }] + dockerfile: contents: """ + FROM daggerio/ci-test:private-pull@sha256:c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060 + """ + } } } diff --git a/tests/tasks/gitpull/private_repo.cue b/tests/tasks/gitpull/private_repo.cue index 028852a2..1bedb4c5 100644 --- a/tests/tasks/gitpull/private_repo.cue +++ b/tests/tasks/gitpull/private_repo.cue @@ -1,24 +1,36 @@ package main -import "dagger.io/dagger/engine" +import ( + "encoding/yaml" + "dagger.io/dagger/engine" +) engine.#Plan & { - inputs: secrets: token: command: { + inputs: secrets: sops: command: { name: "sops" - args: ["exec-env", "../../secrets_sops.yaml", "echo $TestPAT"] + args: ["-d", "../../secrets_sops.yaml"] } actions: { + alpine: engine.#Pull & { source: "alpine:3.15.0" } + repoPassword: engine.#TransformSecret & { + input: inputs.secrets.sops.contents + #function: { + input: _ + output: yaml.Unmarshal(input) + } + } + testRepo: engine.#GitPull & { remote: "https://github.com/dagger/dagger.git" ref: "main" auth: { username: "dagger-test" - password: inputs.secrets.token.contents + password: repoPassword.output.TestPAT.contents } }