1package main
2
3import (
4	"bytes"
5	"encoding/base64"
6	"encoding/json"
7	"flag"
8	"fmt"
9	"github.com/variantdev/vals"
10	"gopkg.in/yaml.v3"
11	"os"
12)
13
14func flagUsage() {
15	text := `vals is a Helm-like configuration "Values" loader with support for various sources and merge strategies
16
17Usage:
18  vals [command]
19
20Available Commands:
21  eval		Evaluate a JSON/YAML document and replace any template expressions in it and prints the result
22  exec		Populates the environment variables and executes the command
23  env		Renders environment variables to be consumed by eval or a tool like direnv
24  ksdecode	Decode YAML document(s) by converting Secret resources' "data" to "stringData" for use with "vals eval"
25
26Use "vals [command] --help" for more infomation about a command
27`
28
29	fmt.Fprintf(os.Stderr, "%s\n", text)
30}
31
32func fatal(format string, args ...interface{}) {
33	fmt.Fprintf(os.Stderr, format+"\n", args...)
34	os.Exit(1)
35}
36
37func readOrFail(f *string) map[string]interface{} {
38	m, err := vals.Input(*f)
39	if err != nil {
40		fatal("%v", err)
41	}
42	return m
43}
44
45func writeOrFail(o *string, res interface{}) {
46	out, err := vals.Output(*o, res)
47	if err != nil {
48		fatal("%v", err)
49	}
50	fmt.Printf("%s", *out)
51}
52
53func main() {
54	flag.Usage = flagUsage
55
56	CmdEval := "eval"
57	CmdExec := "exec"
58	CmdEnv := "env"
59	CmdKsDecode := "ksdecode"
60
61	if len(os.Args) == 1 {
62		flag.Usage()
63		return
64	}
65
66	switch os.Args[1] {
67	case CmdEval:
68		evalCmd := flag.NewFlagSet(CmdEval, flag.ExitOnError)
69		f := evalCmd.String("f", "-", "YAML/JSON file to be evaluated. When set to \"-\", vals reads from STDIN")
70		o := evalCmd.String("o", "yaml", "Output type which is either \"yaml\" or \"json\"")
71		e := evalCmd.Bool("exclude-secret", false, "Leave secretref+<uri> as-is and only replace ref+<uri>")
72		evalCmd.Parse(os.Args[2:])
73
74		m := readOrFail(f)
75
76		res, err := vals.Eval(m, vals.Options{ExcludeSecret: *e})
77		if err != nil {
78			fatal("%v", err)
79		}
80
81		writeOrFail(o, res)
82	case CmdExec:
83		execCmd := flag.NewFlagSet(CmdExec, flag.ExitOnError)
84		f := execCmd.String("f", "", "YAML/JSON file to be loaded to set envvars")
85		execCmd.Parse(os.Args[2:])
86
87		m := readOrFail(f)
88
89		err := vals.Exec(m, execCmd.Args())
90		if err != nil {
91			fatal("%v", err)
92		}
93	case CmdEnv:
94		execEnv := flag.NewFlagSet(CmdEnv, flag.ExitOnError)
95		f := execEnv.String("f", "", "YAML/JSON file to be loaded to set envvars")
96		export := execEnv.Bool("export", false, "Prepend `export`s to each line")
97		execEnv.Parse(os.Args[2:])
98
99		m := readOrFail(f)
100
101		env, err := vals.Env(m)
102		if err != nil {
103			fatal("%v", err)
104		}
105		for _, l := range env {
106			if *export {
107				l = "export " + l
108			}
109			fmt.Fprintln(os.Stdout, l)
110		}
111	case CmdKsDecode:
112		evalCmd := flag.NewFlagSet(CmdKsDecode, flag.ExitOnError)
113		f := evalCmd.String("f", "", "YAML/JSON file to be decoded")
114		o := evalCmd.String("o", "yaml", "Output type which is either \"yaml\" or \"json\"")
115		evalCmd.Parse(os.Args[2:])
116
117		nodes, err := vals.Inputs(*f)
118		if err != nil {
119			fatal("%v", err)
120		}
121
122		var res []yaml.Node
123		for _, node := range nodes {
124			n, err := KsDecode(node)
125			if err != nil {
126				fatal("%v", err)
127			}
128			res = append(res, *n)
129		}
130
131		for i, node := range res {
132			buf := &bytes.Buffer{}
133			encoder := yaml.NewEncoder(buf)
134			encoder.SetIndent(2)
135
136			if err := encoder.Encode(&node); err != nil {
137				fatal("%v", err)
138			}
139			if *o == "json" {
140				var v interface{}
141				if err := json.Unmarshal(buf.Bytes(), &v); err != nil {
142					fatal("%v", err)
143				}
144				bs, err := json.Marshal(v)
145				if err != nil {
146					fatal("%v", err)
147				}
148				print(string(bs))
149			} else {
150				print(buf.String())
151			}
152			if i != len(res)-1 {
153				fmt.Println("---")
154			}
155		}
156	default:
157		flag.Usage()
158	}
159}
160
161func KsDecode(node yaml.Node) (*yaml.Node, error) {
162	if node.Kind != yaml.DocumentNode {
163		return nil, fmt.Errorf("unexpected kind of node: expected %d, got %d", yaml.DocumentNode, node.Kind)
164	}
165
166	var res yaml.Node
167	res = node
168
169	var kk yaml.Node
170	var vv yaml.Node
171	var ii int
172
173	isSecret := false
174	mappings := node.Content[0].Content
175	for i := 0; i < len(mappings); i += 2 {
176		j := i + 1
177		k := mappings[i]
178		v := mappings[j]
179
180		if k.Value == "kind" && v.Value == "Secret" {
181			isSecret = true
182		}
183
184		if k.Value == "data" {
185			ii = i
186			kk = *k
187			vv = *v
188		}
189	}
190
191	if isSecret {
192		kk.Value = "stringData"
193
194		v := vv
195		nestedMappings := v.Content
196		v.Content = make([]*yaml.Node, len(v.Content))
197		for i := 0; i < len(nestedMappings); i += 2 {
198			b64 := nestedMappings[i+1].Value
199			decoded, err := base64.StdEncoding.DecodeString(b64)
200			if err != nil {
201				return nil, err
202			}
203			nestedMappings[i+1].Value = string(decoded)
204
205			v.Content[i] = nestedMappings[i]
206			v.Content[i+1] = nestedMappings[i+1]
207		}
208
209		res.Content[0].Content[ii] = &kk
210		res.Content[0].Content[ii+1] = &v
211	}
212
213	return &res, nil
214}
215