From 5f0fff9db66f815f7f532daeebad14110c37a155 Mon Sep 17 00:00:00 2001 From: nxcc <> Date: Mon, 2 Sep 2024 09:56:50 +0200 Subject: [PATCH] rm alpha3, add alpha4 --- cmd/cuegen/main.go | 14 +- go.mod | 2 +- internal/app/{v1alpha3 => v1alpha4}/main.go | 35 ++- .../app/{v1alpha3 => v1alpha4}/schema.cue | 0 internal/cuegen/v1alpha3/dev-registry.go | 77 ----- internal/cuegen/v1alpha3/handle-attrs.go | 281 ------------------ internal/cuegen/v1alpha3/main.go | 108 ------- internal/cuegen/v1alpha4/main.go | 98 ++++++ internal/cuegen/v1alpha4/wrapper.go | 177 +++++++++++ tests/main_test.go | 11 +- tests/v1alpha3/local/export-path.txtar | 11 - tests/v1alpha3/local/read-env.txtar | 17 -- tests/v1alpha3/local/read-json.txtar | 19 -- tests/v1alpha3/local/read-multi.txtar | 22 -- tests/v1alpha3/local/read-yaml.txtar | 17 -- tests/v1alpha3/local/readfile-multi.txtar | 17 -- tests/v1alpha3/local/readfile-simple.txtar | 15 - tests/v1alpha3/local/readmap-bytes.txtar | 17 -- tests/v1alpha3/local/readmap-dir.txtar | 18 -- tests/v1alpha4/local/sops-secret-only.txtar | 52 ++++ tests/v1alpha4/local/sops-with-template.txtar | 57 ++++ .../local/sops1-decrypt.txtar} | 12 +- 22 files changed, 424 insertions(+), 653 deletions(-) rename internal/app/{v1alpha3 => v1alpha4}/main.go (84%) rename internal/app/{v1alpha3 => v1alpha4}/schema.cue (100%) delete mode 100644 internal/cuegen/v1alpha3/dev-registry.go delete mode 100644 internal/cuegen/v1alpha3/handle-attrs.go delete mode 100644 internal/cuegen/v1alpha3/main.go create mode 100644 internal/cuegen/v1alpha4/main.go create mode 100644 internal/cuegen/v1alpha4/wrapper.go delete mode 100644 tests/v1alpha3/local/export-path.txtar delete mode 100644 tests/v1alpha3/local/read-env.txtar delete mode 100644 tests/v1alpha3/local/read-json.txtar delete mode 100644 tests/v1alpha3/local/read-multi.txtar delete mode 100644 tests/v1alpha3/local/read-yaml.txtar delete mode 100644 tests/v1alpha3/local/readfile-multi.txtar delete mode 100644 tests/v1alpha3/local/readfile-simple.txtar delete mode 100644 tests/v1alpha3/local/readmap-bytes.txtar delete mode 100644 tests/v1alpha3/local/readmap-dir.txtar create mode 100644 tests/v1alpha4/local/sops-secret-only.txtar create mode 100644 tests/v1alpha4/local/sops-with-template.txtar rename tests/{v1alpha3/local/readfile-sops.txtar => v1alpha4/local/sops1-decrypt.txtar} (91%) diff --git a/cmd/cuegen/main.go b/cmd/cuegen/main.go index 5870d70..a82db5b 100644 --- a/cmd/cuegen/main.go +++ b/cmd/cuegen/main.go @@ -3,12 +3,20 @@ package main import ( "os" - v1alpha3 "github.com/noris-network/cuegen/internal/app/v1alpha3" + v1alpha1 "github.com/noris-network/cuegen/internal/app/v1alpha1" + v1alpha4 "github.com/noris-network/cuegen/internal/app/v1alpha4" ) var build = "dev-build" func main() { - v1alpha3.Build = build - os.Exit(v1alpha3.Main()) + v1alpha1.Build = build + v1alpha4.Build = build + + // shortcut to legacy version + if os.Getenv("CUEGEN_COMPATIBILITY_0_14") == "true" { + os.Exit(v1alpha1.Main()) + } + + os.Exit(v1alpha4.Main()) } diff --git a/go.mod b/go.mod index 0e4b755..c2c4714 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/noris-network/cuegen -go 1.22 +go 1.23 require ( cuelang.org/go v0.10.0 diff --git a/internal/app/v1alpha3/main.go b/internal/app/v1alpha4/main.go similarity index 84% rename from internal/app/v1alpha3/main.go rename to internal/app/v1alpha4/main.go index c3451c4..f4225f8 100644 --- a/internal/app/v1alpha3/main.go +++ b/internal/app/v1alpha4/main.go @@ -18,13 +18,13 @@ import ( _ "embed" "flag" "fmt" - "log" + "log/slog" "os" "runtime/debug" "strings" v1alpha1 "github.com/noris-network/cuegen/internal/app/v1alpha1" - cuegen "github.com/noris-network/cuegen/internal/cuegen/v1alpha3" + cuegen "github.com/noris-network/cuegen/internal/cuegen/v1alpha4" "github.com/nxcc/cueconfig" ) @@ -36,11 +36,13 @@ const ( //go:embed schema.cue var configSchema []byte -var Build string -var debugLog = os.Getenv("CUEGEN_DEBUG") == "true" +var ( + Build string + debugLog = os.Getenv("CUEGEN_DEBUG") == "true" + cuegenWrapper = os.Getenv("CUEGEN_SKIP_DECRYPT") != "true" +) func Main() int { - flagIsCuegenDir := false flag.BoolVar(&flagIsCuegenDir, "is-cuegen-dir", false, @@ -48,6 +50,10 @@ func Main() int { flag.Parse() + if debugLog { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + path := "." switch { @@ -62,11 +68,11 @@ func Main() int { printVersion() return 0 - // no chdir + // path = . case len(os.Args) == 1, len(os.Args) == 2 && (os.Args[1] == "." || os.Args[1] == "./"): - // chdir + // path != . case len(os.Args) == 2 && (strings.HasPrefix(os.Args[1], "./") || strings.HasPrefix(os.Args[1], "../") || strings.HasPrefix(os.Args[1], "/")): path = os.Args[1] @@ -81,13 +87,13 @@ func Main() int { fmt.Printf("configure: %v\n", err) return 1 } - if config.ApiVersion != cuegen.V1Alpha3 { + if config.ApiVersion == cuegen.V1Alpha1 { fallbackToV1Alpha1() } - // exec + // exec V1Alpha4 if err := cuegen.Exec(config, path); err != nil { - fmt.Printf("exec: %v\n", err) + fmt.Printf("%v\n", err) return 1 } @@ -107,7 +113,8 @@ func printVersion() { fmt.Printf("cuegen version %v\n", Build) bi, ok := debug.ReadBuildInfo() if !ok { - log.Fatalln("Failed to read build info") + slog.Error("failed to read build info") + os.Exit(1) } for _, dep := range bi.Deps { if dep.Path == "cuelang.org/go" { @@ -120,9 +127,7 @@ func printVersion() { func configure() (cuegen.Cuegen, error) { cfg := struct{ Cuegen cuegen.Cuegen }{} if err := cueconfig.Load(cuegenCue, configSchema, nil, nil, &cfg); err != nil { - if os.IsNotExist(err) { - return cuegen.Default, nil - } + slog.Error("load cuegen.cue", "err", err) return cuegen.Cuegen{}, err } return cfg.Cuegen, nil @@ -130,7 +135,7 @@ func configure() (cuegen.Cuegen, error) { func fallbackToV1Alpha1() { if debugLog { - fmt.Println("#@@@ fallback to v1alpha1") + slog.Debug("fallback to v1alpha1") } os.Exit(v1alpha1.Main()) } diff --git a/internal/app/v1alpha3/schema.cue b/internal/app/v1alpha4/schema.cue similarity index 100% rename from internal/app/v1alpha3/schema.cue rename to internal/app/v1alpha4/schema.cue diff --git a/internal/cuegen/v1alpha3/dev-registry.go b/internal/cuegen/v1alpha3/dev-registry.go deleted file mode 100644 index ca64a6d..0000000 --- a/internal/cuegen/v1alpha3/dev-registry.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2024 cuegen Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cuegen - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "cuelang.org/go/mod/modconfig" - "cuelang.org/go/mod/module" -) - -type DevRegistry struct { - registry modconfig.Registry - root string -} - -func NewDevRegistry(root string) modconfig.Registry { - if debugLog { - fmt.Printf("#@@@ NewDevRegistry: %v\n", root) - } - registry, err := modconfig.NewRegistry(nil) - if err != nil { - panic(err) - } - return DevRegistry{registry: registry, root: root} -} - -func (r DevRegistry) Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error) { - loc, err := r.getDevLoc(m) - if err != nil { - if debugLog { - fmt.Printf("#@@@ getLoc %v: %v\n", m, err) - } - loc, err = r.registry.Fetch(ctx, m) - } - return loc, err -} - -func (r DevRegistry) getDevLoc(m module.Version) (module.SourceLoc, error) { - path := filepath.Join(r.root, m.BasePath()) - fileInfo, err := os.Stat(path) - if err != nil { - return module.SourceLoc{}, err - } - if !fileInfo.IsDir() { - return module.SourceLoc{}, fmt.Errorf("%v is not a dir", path) - } - if debugLog { - fmt.Printf("#@@@ %v ==> %v\n", m.String(), path) - } - return module.SourceLoc{FS: module.OSDirFS(path), Dir: "."}, nil -} - -func (r DevRegistry) Requirements(ctx context.Context, m module.Version) ([]module.Version, error) { - panic("not handled: Requirements") - return r.registry.Requirements(ctx, m) -} - -func (r DevRegistry) ModuleVersions(ctx context.Context, mpath string) ([]string, error) { - panic("not handled: ModuleVersions") - return r.registry.ModuleVersions(ctx, mpath) -} diff --git a/internal/cuegen/v1alpha3/handle-attrs.go b/internal/cuegen/v1alpha3/handle-attrs.go deleted file mode 100644 index 5511c70..0000000 --- a/internal/cuegen/v1alpha3/handle-attrs.go +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2024 cuegen Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cuegen - -import ( - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "slices" - "strings" - - "gopkg.in/yaml.v3" - - "cuelang.org/go/cue" - "github.com/getsops/sops/v3/cmd/sops/formats" - "github.com/getsops/sops/v3/decrypt" - "github.com/joho/godotenv" -) - -type pathValueAttributes map[string]valueAttributes - -type valueAttributes struct { - attrs []cue.Attribute - subPath string -} - -var cuegenAttrs = []string{"read", "readfile", "readmap"} - -func findAttributes(value cue.Value) pathValueAttributes { - - // collect cuegen attributes with path - paths := pathValueAttributes{} - seen := map[string]bool{} - value.Walk(func(v cue.Value) bool { - attrs := []cue.Attribute{} - for _, attr := range v.Attributes(cue.ValueAttr) { - if slices.Contains(cuegenAttrs, attr.Name()) { - attrs = append(attrs, attr) - } - } - if len(attrs) > 0 { - filename := v.Pos().Filename() - subPath, _ := strings.CutPrefix(filename, "cg.ChartRoot"+"/") - cuePath := v.Path().String() - attrsContents := "" - seenKey := fmt.Sprintf("%v:%v:%v", subPath, v.Pos().Offset(), attrsContents) - if seen[seenKey] { - return true - } - paths[cuePath] = valueAttributes{ - attrs: attrs, - subPath: subPath, - } - seen[seenKey] = true - } - return true - }, func(v cue.Value) {}) - return paths -} - -func processAttributes(value cue.Value, attrs pathValueAttributes) (cue.Value, error) { - for cuePath, valAttr := range attrs { - bpath := filepath.Dir(valAttr.subPath) - for _, attr := range valAttr.attrs { - if attr.Contents() == "" { - return cue.Value{}, fmt.Errorf("empty attribute: @%v() at <%s>", attr.Name(), cuePath) - } - var err error - switch attr.Name() { - case "readfile": - value, err = attrReadFile(value, cuePath, attr, bpath) - case "readmap": - value, err = attrReadMap(value, cuePath, attr, bpath) - case "read": - value, err = attrRead(value, cuePath, attr, bpath) - default: - panic("unknown attribute") - } - if err != nil { - return cue.Value{}, fmt.Errorf("@%v at <%s>: %v", attr.Name(), cuePath, err) - } - } - } - return value, nil -} - -func attrReadFile(value cue.Value, path string, attr cue.Attribute, bpath string) (cue.Value, error) { - alldata := "" - bytesFlag := "" - for i := 0; i < attr.NumArgs(); i++ { - file, flag := attr.Arg(i) - data, err := readFile(filepath.Join(bpath, file)) - if err != nil { - return value, fmt.Errorf("attrReadFile: %v", err) - } - switch flag { - case "nl": - alldata = alldata + strings.TrimRight(data, "\n") + "\n" - case "trim": - alldata = alldata + strings.TrimSpace(data) - case "bytes": - bytesFlag = flag - fallthrough - default: - alldata = alldata + data - } - } - if asBytes(bytesFlag) { - value = value.FillPath(cue.ParsePath(path), []byte(alldata)) - } else { - value = value.FillPath(cue.ParsePath(path), alldata) - } - return value, nil -} - -func attrRead(value cue.Value, cuePath string, attr cue.Attribute, bpath string) (cue.Value, error) { - for i := 0; i < attr.NumArgs(); i++ { - item, _ := attr.Arg(i) - data, err := readPath(filepath.Join(bpath, item)) - if err != nil { - return value, fmt.Errorf("attrRead: %v", err) - } - value = value.FillPath(cue.ParsePath(cuePath), data) - } - return value, nil -} - -func readFile(file string) (string, error) { - data, err := os.ReadFile(file) - if err != nil { - return "", fmt.Errorf("readFile: %q: %v", file, err) - } - - var format formats.Format - var probes []string - stringData := string(data) - - // guess file format from extension - switch filepath.Ext(file) { - case ".env": - format = formats.Dotenv - probes = probeStringsEnv - case ".yml": - fallthrough - case ".yaml": - format = formats.Yaml - probes = probeStringsJsonOrYamlOrBinary - case ".json": - format = formats.Json - probes = probeStringsJsonOrYamlOrBinary - default: - format = formats.Binary - probes = probeStringsJsonOrYamlOrBinary - } - - // detect whether contents looks like sops encrypted - allMatched := true - for _, probe := range probes { - allMatched = allMatched && strings.Contains(stringData, probe) - } - if allMatched { - plaintext, err := decrypt.DataWithFormat(data, format) - if err == nil { - return string(plaintext), nil - } - log.Printf("WARN: %q: looks like sops encrypted, but decrypt failed: %v", file, err) - } - - return stringData, nil -} - -func attrReadMap(value cue.Value, cuePath string, attr cue.Attribute, bpath string) (cue.Value, error) { - for i := 0; i < attr.NumArgs(); i++ { - item, suffix := attr.Arg(i) - data, err := readPath(filepath.Join(bpath, item)) - if err != nil { - return value, fmt.Errorf("attrRead: %v", err) - } - for k, v := range data { - pathItems := cue.ParsePath(fmt.Sprintf("%v.%q", cuePath, k)) - switch stringValue := v.(type) { - case string: - if asBytes(suffix) { - value = value.FillPath(pathItems, []byte(stringValue)) - } else { - value = value.FillPath(pathItems, stringValue) - } - default: - return value, fmt.Errorf("value of type %T not allowed with readmap", v) - } - } - } - return value, nil -} - -func readPath(path string) (map[string]any, error) { - fileinfo, err := os.Stat(path) - if err != nil { - return nil, fmt.Errorf("readPath: %v", err) - } - if fileinfo.IsDir() { - entries, err := os.ReadDir(path) - if err != nil { - return nil, fmt.Errorf("readPath: %v", err) - } - data := map[string]any{} - for _, entry := range entries { - if !entry.Type().IsRegular() { - continue - } - stringData, err := readFile(filepath.Join(path, entry.Name())) - if err != nil { - return nil, fmt.Errorf("readPath: %v", err) - } - data[entry.Name()] = stringData - } - return data, nil - } - if fileinfo.Mode().IsRegular() { - return readStructFile(path) - } - return nil, fmt.Errorf("readPath: can't handle %q", path) -} - -func asBytes(suffix string) bool { - return suffix == "bytes" -} - -func readStructFile(file string) (map[string]any, error) { - contents, err := readFile(file) - if err != nil { - return nil, fmt.Errorf("readStructFile: %q: %v", file, err) - } - fileExt := filepath.Ext(file) - data := map[string]any{} - switch fileExt { - case ".env": - env, err := godotenv.Unmarshal(contents) - if err != nil { - return data, fmt.Errorf("readStructFile: %q: %v", file, err) - } - for k, v := range env { - data[k] = v - } - case ".yml", ".yaml": - err = yaml.Unmarshal([]byte(contents), &data) - if err != nil { - return data, fmt.Errorf("readStructFile: %q: %v", file, err) - } - case ".json": - err = json.Unmarshal([]byte(contents), &data) - if err != nil { - return data, fmt.Errorf("readStructFile: %q: %v", file, err) - } - } - return data, nil -} - -// probeStrings for "sops" detection -var ( - probeStringsJsonOrYamlOrBinary = []string{ - "sops", "version", "unencrypted_suffix", "lastmodified", "mac", - } - probeStringsEnv = []string{ - "sops_version", "sops_unencrypted_suffix", "sops_lastmodified", "sops_mac", - } -) diff --git a/internal/cuegen/v1alpha3/main.go b/internal/cuegen/v1alpha3/main.go deleted file mode 100644 index 6e7dd5d..0000000 --- a/internal/cuegen/v1alpha3/main.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2024 cuegen Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cuegen - -import ( - "fmt" - "os" - "strings" - - "cuelang.org/go/cue" - "cuelang.org/go/cue/cuecontext" - "cuelang.org/go/cue/load" - cueyaml "cuelang.org/go/pkg/encoding/yaml" -) - -const ( - V1Alpha3 = "v1alpha3" -) - -type Cuegen struct { - ApiVersion string - Metadata struct { - Version struct { - Pkg string - App string - Pre string - } - } - Spec Spec -} - -type Spec struct { - Export string -} - -var Default = Cuegen{ - ApiVersion: V1Alpha3, - Spec: Spec{Export: "objects"}, -} - -var debugLog = os.Getenv("CUEGEN_DEBUG") == "true" - -func Exec(config Cuegen, path string) error { - - // new context - ctx := cuecontext.New(cuecontext.EvaluatorVersion(cuecontext.EvalDefault)) - - // prepare load.Config - loadConfig := load.Config{} - if loMo := os.Getenv("CUEGEN_USE_LOCAL_MODULES"); loMo != "" { - loadConfig.Registry = NewDevRegistry(loMo) - } - - // only load one instance - instance := load.Instances([]string{path}, &loadConfig)[0] - if instance.Err != nil { - return fmt.Errorf("load instance: %v", instance.Err) - } - - value := ctx.BuildInstance(instance) - if err := value.Err(); err != nil { - return fmt.Errorf("build instance: %v", value.Err()) - } - - // handle cuegen attributes - paths := findAttributes(value) - value, err := processAttributes(value, paths) - if err != nil { - return fmt.Errorf("process attributes: %v", value.Err()) - } - - // generate export - exportPath := cue.ParsePath(config.Spec.Export) - if exportPath.Err() != nil { - return fmt.Errorf("parse export path: %v", exportPath.Err()) - } - export := value.LookupPath(exportPath) - if export.Err() != nil { - return fmt.Errorf("lookup path: %v", export.Err()) - } - - // dump objects to yaml - yamlString, err := cueyaml.MarshalStream(export) - if err != nil { - return fmt.Errorf("marshal stream: %v", instance.Err) - } - - fixedYamlString := noBinaryPrefix(yamlString) - fmt.Print(fixedYamlString) - - return nil -} - -func noBinaryPrefix(yml string) string { - return strings.ReplaceAll(yml, ": !!binary ", ": ") -} diff --git a/internal/cuegen/v1alpha4/main.go b/internal/cuegen/v1alpha4/main.go new file mode 100644 index 0000000..d94a3b2 --- /dev/null +++ b/internal/cuegen/v1alpha4/main.go @@ -0,0 +1,98 @@ +// Copyright 2024 cuegen Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cuegen + +import ( + "fmt" + "log/slog" + "os" + "strings" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/interpreter/embed" + "cuelang.org/go/cue/load" + "cuelang.org/go/pkg/encoding/yaml" +) + +const ( + V1Alpha1 = "v1alpha1" + V1Alpha4 = "v1alpha4" +) + +type Cuegen struct { + ApiVersion string + Metadata struct { + Version struct { + Pkg string + App string + Pre string + } + } + Spec Spec +} + +type Spec struct { + Export string +} + +var ( + Default = Cuegen{ + ApiVersion: V1Alpha4, + Spec: Spec{Export: "objects"}, + } + cuegenWrapper = os.Getenv("CUEGEN_SKIP_DECRYPT") != "true" +) + +func Exec(config Cuegen, path string) error { + if cuegenWrapper { + execWrapper() + } + + slog := slog.With("app", "cuegen") + + os.Setenv("CUE_EXPERIMENT", "embed") + ctx := cuecontext.New(cuecontext.Interpreter(embed.New())) + instance := load.Instances([]string{path}, nil)[0] + if instance.Err != nil { + slog.Debug("load instance", "err", instance.Err) + return fmt.Errorf("cue: %v", instance.Err) + } + value := ctx.BuildInstance(instance) + if value.Err() != nil { + slog.Debug("build instance", "err", value.Err()) + return fmt.Errorf("cue: %v", value.Err()) + } + objects := value.LookupPath(cue.ParsePath(config.Spec.Export)) + + err := objects.Validate(cue.Concrete(true), cue.Final()) + if err != nil { + slog.Debug("validate", "err", err) + return fmt.Errorf("cue: %v", err) + } + + yamlString, err := yaml.MarshalStream(objects) + // yamlString, err := yaml.MarshalStream(value) + if err != nil { + slog.Debug("marshal", "err", err) + return fmt.Errorf("marshal stream: %v", err) + } + fmt.Print(noBinaryPrefix(yamlString)) + return nil +} + +func noBinaryPrefix(yml string) string { + return strings.ReplaceAll(yml, ": !!binary ", ": ") +} diff --git a/internal/cuegen/v1alpha4/wrapper.go b/internal/cuegen/v1alpha4/wrapper.go new file mode 100644 index 0000000..c62b680 --- /dev/null +++ b/internal/cuegen/v1alpha4/wrapper.go @@ -0,0 +1,177 @@ +// Copyright 2024 cuegen Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cuegen + +import ( + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/getsops/sops/v3/cmd/sops/formats" + "github.com/getsops/sops/v3/decrypt" +) + +var ( + backupPrefix = ".cuegen-backup-" + strconv.Itoa(os.Getpid()) + "~" + restoreAfterRun = map[string]string{} + knownSopsFormats = map[string]formats.Format{ + ".env": formats.Dotenv, + ".json": formats.Json, + ".sops": formats.Binary, + ".yaml": formats.Yaml, + ".yml": formats.Yaml, + } +) + +var wlog = slog.With("app", "wrapper") + +func execWrapper() { + dir, err := os.Getwd() + if err != nil { + wlog.Error("get wd", "err", err) + os.Exit(1) + } + dir = filepath.Clean(dir) + + // decrypt sops files + if err := decryptPath(dir); err != nil { + wlog.Error("decrypt", "err", err) + os.Exit(1) + } + + // execute cuegen or other wrapped executable + wexe := os.Getenv("CUEGEN_WRAPPED_EXECUTABLE") + if wexe == "" { + exe, err := os.Executable() + if err != nil { + wlog.Error("find executable", "err", err) + os.Exit(1) + } + wexe = exe + } + cmd := exec.Command(wexe, os.Args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Env = append(os.Environ(), "CUEGEN_SKIP_DECRYPT=true") + if err := cmd.Run(); err != nil { + wlog.Debug("exec", "exe", wexe, "err", err) + } + + // restore original state + if err := restorePath(); err != nil { + wlog.Error("restore failed", "err", err) + os.Exit(1) + } + + os.Exit(0) +} + +func decryptPath(path string) error { + slog.Debug("decrypt", "path", path) + return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + if os.IsNotExist(err) { + return nil + } else { + return err + } + } + if !strings.Contains(path, ".sops.") && !strings.HasSuffix(path, ".sops") { + return nil + } + if info.Name() == ".sops.yaml" { + return nil + } + return decryptFile(path) + }) +} + +func decryptFile(path string) error { + slog.Debug("decrypt", "file", path) + ext := filepath.Ext(path) + if _, found := knownSopsFormats[ext]; !found { + wlog.Warn("found unhandled extension", "ext", ext) + return nil + } + nonSopsPath := toNonSopsPath(path) + if nonSopsPath == "" { + wlog.Warn("skip", "file", path) + return nil + } + _, err := os.Stat(nonSopsPath) + if err == nil { + err := backupFile(nonSopsPath) + if err != nil { + return fmt.Errorf("%v: can not backup file: %v", path, err) + } + } else { + restoreAfterRun[nonSopsPath] = "" + } + + wlog.Debug("decrypt", "source", path, "target", nonSopsPath) + cleartext, err := decrypt.File(path, ext) + if err != nil { + return fmt.Errorf("%v: can not open decrypt file: %v", path, err) + } + f, err := os.Create(nonSopsPath) + if err != nil { + return fmt.Errorf("%v: %v", path, err) + } + f.Write(cleartext) + f.Close() + return nil +} + +func backupFile(path string) error { + bak := filepath.Join(filepath.Dir(path), backupPrefix+filepath.Base(path)) + wlog.Debug("backup", "source", path, "target", bak) + restoreAfterRun[path] = bak + return os.Rename(path, bak) +} + +func restorePath() error { + for orig, bak := range restoreAfterRun { + if bak == "" { + wlog.Debug("remove", "file", orig) + if err := os.Remove(orig); err != nil { + return fmt.Errorf("cleanup: remove failed: %v: %v", orig, err) + } + continue + } + wlog.Debug("restore", "target", orig, "source", bak) + if err := os.Rename(bak, orig); err != nil { + return fmt.Errorf("cleanup: restore failed: %v: %v", orig, err) + } + } + return nil +} + +func toNonSopsPath(path string) string { + if strings.HasSuffix(path, ".sops") { + return strings.TrimSuffix(path, ".sops") + } + ext := filepath.Ext(path) + path = strings.TrimSuffix(path, ext) + sops := filepath.Ext(path) + if sops != ".sops" { + return "" + } + return strings.TrimSuffix(path, sops) + ext +} diff --git a/tests/main_test.go b/tests/main_test.go index dada6e3..11ec90b 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -20,14 +20,13 @@ import ( "os" "testing" - v1alpha3 "github.com/noris-network/cuegen/internal/app/v1alpha3" + v1alpha4 "github.com/noris-network/cuegen/internal/app/v1alpha4" "github.com/rogpeppe/go-internal/testscript" ) func TestMain(m *testing.M) { os.Exit(testscript.RunMain(m, map[string]func() int{ - //"cuegen": app.Main, - "cuegen": v1alpha3.Main, + "cuegen": v1alpha4.Main, "started_from_go_test": func() int { return 0 }, })) } @@ -38,12 +37,6 @@ func TestCuegenLocalV1alpha1(t *testing.T) { }) } -func TestCuegenLocalV1alpha3(t *testing.T) { - testscript.Run(t, testscript.Params{ - Dir: "v1alpha3/local", - }) -} - func TestCuegenRemoteV1alpha1(t *testing.T) { testscript.Run(t, testscript.Params{ Dir: "v1alpha1/remote", diff --git a/tests/v1alpha3/local/export-path.txtar b/tests/v1alpha3/local/export-path.txtar deleted file mode 100644 index cae6232..0000000 --- a/tests/v1alpha3/local/export-path.txtar +++ /dev/null @@ -1,11 +0,0 @@ -### export-path.txtar - -exec cuegen -stdout 'c: ccc' --- cuegen.cue -- -cuegen: apiVersion: "v1alpha3" -cuegen: spec: export: "myobjects" --- file.cue -- -package foo - -myobjects: [{a: b: {c: "ccc"}}] diff --git a/tests/v1alpha3/local/read-env.txtar b/tests/v1alpha3/local/read-env.txtar deleted file mode 100644 index 2a83133..0000000 --- a/tests/v1alpha3/local/read-env.txtar +++ /dev/null @@ -1,17 +0,0 @@ -### read-env.txtar - -exec cuegen -stdout 'BAR: "42' -stdout 'FOO: "23' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data.env -- -FOO=23 -BAR=42 --- file.cue -- -package foo - -objects: [{a: b: { - c: {} @read(data.env) -}}] diff --git a/tests/v1alpha3/local/read-json.txtar b/tests/v1alpha3/local/read-json.txtar deleted file mode 100644 index 17ec02e..0000000 --- a/tests/v1alpha3/local/read-json.txtar +++ /dev/null @@ -1,19 +0,0 @@ -### read-json.txtar - -exec cuegen -stdout 'bar: 42' -stdout 'foo: 23' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data.json -- -{ - "foo": 23, - "bar": 42 -} --- file.cue -- -package foo - -objects: [{a: b: { - c: {} @read(data.json) -}}] diff --git a/tests/v1alpha3/local/read-multi.txtar b/tests/v1alpha3/local/read-multi.txtar deleted file mode 100644 index 94631d1..0000000 --- a/tests/v1alpha3/local/read-multi.txtar +++ /dev/null @@ -1,22 +0,0 @@ -### read-multi.txtar - -exec cuegen -stdout 'bar: 42' -stdout 'foo: 23' -stdout 'baz: twentythree' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data.json -- -{ - "baz": "twentythree" -} --- data.yaml -- -foo: 23 -bar: 42 --- file.cue -- -package foo - -objects: [{a: b: { - c: {} @read(data.yaml, data.json) -}}] diff --git a/tests/v1alpha3/local/read-yaml.txtar b/tests/v1alpha3/local/read-yaml.txtar deleted file mode 100644 index ef01993..0000000 --- a/tests/v1alpha3/local/read-yaml.txtar +++ /dev/null @@ -1,17 +0,0 @@ -### read-yaml.txtar - -exec cuegen -stdout 'bar: 42' -stdout 'foo: 23' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data.yaml -- -foo: 23 -bar: 42 --- file.cue -- -package foo - -objects: [{a: b: { - c: {} @read(data.yaml) -}}] diff --git a/tests/v1alpha3/local/readfile-multi.txtar b/tests/v1alpha3/local/readfile-multi.txtar deleted file mode 100644 index 3db002c..0000000 --- a/tests/v1alpha3/local/readfile-multi.txtar +++ /dev/null @@ -1,17 +0,0 @@ -### readfile-multi.txtar - -exec cuegen -stdout 'foo.bar.baz-red/green/blue' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data-rgb.txt -- --red/green/blue --- data.txt -- -foo.bar.baz --- file.cue -- -package foo - -objects: [{a: b: { - c: *"default" | string @readfile(data.txt=trim, data-rgb.txt=trim) -}}] diff --git a/tests/v1alpha3/local/readfile-simple.txtar b/tests/v1alpha3/local/readfile-simple.txtar deleted file mode 100644 index 9f5f045..0000000 --- a/tests/v1alpha3/local/readfile-simple.txtar +++ /dev/null @@ -1,15 +0,0 @@ -### readfile-simple.txtar - -exec cuegen -stdout 'foo.bar.baz' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data.txt -- -foo.bar.baz --- file.cue -- -package foo - -objects: [{a: b: { - c: *"default" | string @readfile(data.txt) -}}] diff --git a/tests/v1alpha3/local/readmap-bytes.txtar b/tests/v1alpha3/local/readmap-bytes.txtar deleted file mode 100644 index 76864d5..0000000 --- a/tests/v1alpha3/local/readmap-bytes.txtar +++ /dev/null @@ -1,17 +0,0 @@ -### readmap-bytes.txtar - -exec cuegen -stdout 'dHdlbnR5dGhyZWU=' -stdout 'Zm9ydHl0d28=' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data.yaml -- -foo: twentythree -bar: fortytwo --- file.cue -- -package foo - -objects: [{a: b: { - c: {} @readmap(data.yaml=bytes) -}}] diff --git a/tests/v1alpha3/local/readmap-dir.txtar b/tests/v1alpha3/local/readmap-dir.txtar deleted file mode 100644 index a2865a8..0000000 --- a/tests/v1alpha3/local/readmap-dir.txtar +++ /dev/null @@ -1,18 +0,0 @@ -### readmap-dir.txtar - -exec cuegen -stdout 'twentythree' -stdout 'fortytwo' --- cue.mod/module.cue -- -module: "example.com/foo@v0" -language: version: "v0.9.0" --- data/bar -- -fortytwo --- data/foo -- -twentythree --- file.cue -- -package foo - -objects: [{a: b: { - c: {} @readmap(data) -}}] diff --git a/tests/v1alpha4/local/sops-secret-only.txtar b/tests/v1alpha4/local/sops-secret-only.txtar new file mode 100644 index 0000000..0c768f0 --- /dev/null +++ b/tests/v1alpha4/local/sops-secret-only.txtar @@ -0,0 +1,52 @@ +### sops-secret-only.txtar + +env SOPS_AGE_KEY_FILE=$WORK/keys.txt +env CUEGEN_WRAPPED_EXECUTABLE=find + +exec find +! stdout .cuegen-backup-.+~secret.txt +! stdout secret.txt$ +stdout secret.txt.sops + +exec /tmp/cuegen-v1alpha4 +! stdout .cuegen-backup-.+~secret.txt +stdout secret.txt$ +stdout secret.txt.sops + +exec find +! stdout .cuegen-backup-.+~secret.txt +! stdout secret.txt$ +stdout secret.txt.sops +-- file.cue -- +@extern(embed) + +package kube + +objects: [{text: _ @embed(file="secret.txt")}] +-- keys.txt -- +# * * * this key is just for testing, never use it for anything else * * * +AGE-SECRET-KEY-14QUHLE5A6UNSKNYXLF5ZA26P3NCFX8P68JQ066T7VJ6JW5G8FHWQN4HAUQ +# * * * this key is just for testing, never use it for anything else * * * + +-- secret.txt.sops -- +{ + "data": "ENC[AES256_GCM,data:NDgRD9FS0p0M5WKnhUFtv0U=,iv:PcUAVThKzEz0FmgG610GoHPMMuG0nzIOiTfrfsf9AK4=,tag:NTVqjyfshEBzctAMVyZowg==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age13643rcqprsmy33ff4rgj2strpyhxgzu3x6lvyrzvhsqqjvmk9d3qe59qn8", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuMm1wR0hTUHBYZlVjczNU\nRWxxMFhpbTlZQVB5aFZBYzd0VVE4N0JmWnlZCitFdkxHazhXTDFkbUFyNHp0bStI\nVGx3dC9Yc0hrS3lhakRRdUg3TzA4TXMKLS0tIEwrWWZzbFEydHVUc3RVS3NKQ3lL\nSC8rWm5XYTFLUXdURXJDL08yN00vVE0K1oqiXcBR7tZh342LBReJYrVTxekJ/sq2\nQTE4oweuyjtOp55wSUW8cSiIw7uABHj93zE0OTn9EEv/5aDYYN53AA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2023-01-06T13:55:03Z", + "mac": "ENC[AES256_GCM,data:jbj9mfdj0/cBrtZr8biapIGh7qGWdgpVs6JkXwN62Wa2VEtI00CZtJpXazVyEkpw7o1+Q6eOpE+rfLPm18IbE5blyrXFKWY7reptkGdVDmI38mBvzRsirYBdTWM27ufJElFaYdLQVyQ+d1vGHdl3NxQAF5DLg9lGlPnF6jlHq58=,iv:wJvOUhC/7b1ZTeneXKcYWSFHNdjpFEqVd6zwv7JfLko=,tag:2AFEkXUA1fMPt7OelOO8Eg==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.3" + } +} + diff --git a/tests/v1alpha4/local/sops-with-template.txtar b/tests/v1alpha4/local/sops-with-template.txtar new file mode 100644 index 0000000..ef0281b --- /dev/null +++ b/tests/v1alpha4/local/sops-with-template.txtar @@ -0,0 +1,57 @@ +### sops-with-template.txtar + +env SOPS_AGE_KEY_FILE=$WORK/keys.txt +env CUEGEN_WRAPPED_EXECUTABLE=find + +exec find +! stdout .cuegen-backup-.+~secret.txt +stdout secret.txt$ +stdout secret.txt.sops + +exec /tmp/cuegen-v1alpha4 +stdout .cuegen-backup-.+~secret.txt +stdout secret.txt$ +stdout secret.txt.sops + +exec find +! stdout .cuegen-backup-.+~secret.txt +stdout secret.txt$ +stdout secret.txt.sops + +exec cat secret.txt +stdout 'no secrets here' +-- file.cue -- +@extern(embed) + +package kube + +objects: [{text: _ @embed(file="secret.txt")}] +-- keys.txt -- +# * * * this key is just for testing, never use it for anything else * * * +AGE-SECRET-KEY-14QUHLE5A6UNSKNYXLF5ZA26P3NCFX8P68JQ066T7VJ6JW5G8FHWQN4HAUQ +# * * * this key is just for testing, never use it for anything else * * * + +-- secret.txt -- +no secrets here +-- secret.txt.sops -- +{ + "data": "ENC[AES256_GCM,data:NDgRD9FS0p0M5WKnhUFtv0U=,iv:PcUAVThKzEz0FmgG610GoHPMMuG0nzIOiTfrfsf9AK4=,tag:NTVqjyfshEBzctAMVyZowg==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age13643rcqprsmy33ff4rgj2strpyhxgzu3x6lvyrzvhsqqjvmk9d3qe59qn8", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuMm1wR0hTUHBYZlVjczNU\nRWxxMFhpbTlZQVB5aFZBYzd0VVE4N0JmWnlZCitFdkxHazhXTDFkbUFyNHp0bStI\nVGx3dC9Yc0hrS3lhakRRdUg3TzA4TXMKLS0tIEwrWWZzbFEydHVUc3RVS3NKQ3lL\nSC8rWm5XYTFLUXdURXJDL08yN00vVE0K1oqiXcBR7tZh342LBReJYrVTxekJ/sq2\nQTE4oweuyjtOp55wSUW8cSiIw7uABHj93zE0OTn9EEv/5aDYYN53AA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2023-01-06T13:55:03Z", + "mac": "ENC[AES256_GCM,data:jbj9mfdj0/cBrtZr8biapIGh7qGWdgpVs6JkXwN62Wa2VEtI00CZtJpXazVyEkpw7o1+Q6eOpE+rfLPm18IbE5blyrXFKWY7reptkGdVDmI38mBvzRsirYBdTWM27ufJElFaYdLQVyQ+d1vGHdl3NxQAF5DLg9lGlPnF6jlHq58=,iv:wJvOUhC/7b1ZTeneXKcYWSFHNdjpFEqVd6zwv7JfLko=,tag:2AFEkXUA1fMPt7OelOO8Eg==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.3" + } +} + diff --git a/tests/v1alpha3/local/readfile-sops.txtar b/tests/v1alpha4/local/sops1-decrypt.txtar similarity index 91% rename from tests/v1alpha3/local/readfile-sops.txtar rename to tests/v1alpha4/local/sops1-decrypt.txtar index b605ee5..0b4bd8c 100644 --- a/tests/v1alpha3/local/readfile-sops.txtar +++ b/tests/v1alpha4/local/sops1-decrypt.txtar @@ -1,20 +1,20 @@ -### readfile-sops.txtar +### sops1-decrypt.txtar env SOPS_AGE_KEY_FILE=$WORK/keys.txt -exec cuegen +exec /tmp/cuegen-v1alpha4 stdout 'some secret data' -- file.cue -- +@extern(embed) + package kube -objects: [{a: b: { - c: *"default" | string @readfile(secret.txt=trim) -}}] +objects: [{text: _ @embed(file="secret.txt")}] -- keys.txt -- # * * * this key is just for testing, never use it for anything else * * * AGE-SECRET-KEY-14QUHLE5A6UNSKNYXLF5ZA26P3NCFX8P68JQ066T7VJ6JW5G8FHWQN4HAUQ # * * * this key is just for testing, never use it for anything else * * * --- secret.txt -- +-- secret.txt.sops -- { "data": "ENC[AES256_GCM,data:NDgRD9FS0p0M5WKnhUFtv0U=,iv:PcUAVThKzEz0FmgG610GoHPMMuG0nzIOiTfrfsf9AK4=,tag:NTVqjyfshEBzctAMVyZowg==,type:str]", "sops": {