diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-05-17 07:20:23 -0500 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2024-05-17 07:28:19 -0500 |
| commit | 7ce28bfc8ac4f306196bacbc4ff06671e8a58654 (patch) | |
| tree | ac0b266643e83333257e9bf5c04df6e425b0445d | |
| parent | f22aa000db6e91f0fb59f4728bdbda0aa40f475d (diff) | |
| download | x-7ce28bfc8ac4f306196bacbc4ff06671e8a58654.tar.xz x-7ce28bfc8ac4f306196bacbc4ff06671e8a58654.zip | |
internal: add package flagfolder to populate FlagSets with a secret mount
Signed-off-by: Xe Iaso <me@xeiaso.net>
| -rw-r--r-- | .go.mod.sri | 2 | ||||
| -rw-r--r-- | flagfolder/flagfolder.go | 110 | ||||
| -rw-r--r-- | flagfolder/flagfolder_test.go | 44 | ||||
| -rw-r--r-- | flagfolder/testdata/FOO | 1 | ||||
| -rw-r--r-- | flagfolder/testdata/WHAT_IS_COMPUTER | 1 | ||||
| -rw-r--r-- | flagfolder/testdata/bar | 1 | ||||
| -rw-r--r-- | flagfolder/testdata/something_here | 1 | ||||
| -rw-r--r-- | flagfolder/testpod.yml | 31 | ||||
| -rw-r--r-- | flake.nix | 2 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | internal/internal.go | 5 |
12 files changed, 198 insertions, 3 deletions
diff --git a/.go.mod.sri b/.go.mod.sri index 56dd64a..29942dc 100644 --- a/.go.mod.sri +++ b/.go.mod.sri @@ -1 +1 @@ -sha256-hbKmwwxDWQqwJpym2wFOTJCc5ByZTM7WBQM2l5yXuDw= +sha256-YARBn+bGzZ3FNAfyMdsWFeRDiOKp9XShbtIIAFIchlM= diff --git a/flagfolder/flagfolder.go b/flagfolder/flagfolder.go new file mode 100644 index 0000000..cb0d249 --- /dev/null +++ b/flagfolder/flagfolder.go @@ -0,0 +1,110 @@ +// Package flagfolder parses a folder on the disk as if each file in it had the contents of a command line flag. +// +// This is mainly intended to be used with environments like Kubernetes where you have your secrets mounted as a filesystem. +package flagfolder + +import ( + "flag" + "fmt" + "log/slog" + "os" + "path/filepath" + + "github.com/stoewer/go-strcase" +) + +// ParseSet parses secrets in a single folder into the given *flag.FlagSet. +// +// By default this will attempt to correct for several styles of naming for files: +// +// * kebab-case (the default) +// * SHOUTING-KEBAB-CASE +// * snake_case +// * SHOUTING_SNAKE_CASE +// * camelCase +// * HammerCase +func ParseSet(secretLocation string, set *flag.FlagSet) error { + var ( + data []byte + err error + ) + + set.VisitAll(func(f *flag.Flag) { + if err != nil { + return + } + + for _, fname := range []string{ + filepath.Join(secretLocation, f.Name), + filepath.Join(secretLocation, strcase.UpperKebabCase(f.Name)), + filepath.Join(secretLocation, strcase.LowerCamelCase(f.Name)), + filepath.Join(secretLocation, strcase.UpperCamelCase(f.Name)), + filepath.Join(secretLocation, strcase.SnakeCase(f.Name)), + filepath.Join(secretLocation, strcase.UpperSnakeCase(f.Name)), + } { + data, err = os.ReadFile(fname) + if err != nil { + slog.Debug("can't read", "fname", fname, "err", err) + if os.IsNotExist(err) { + err = nil + continue + } + continue + } + } + + if ferr := f.Value.Set(string(data)); ferr != nil { + err = fmt.Errorf("flagfolder: failed to set flag %q with value %q", f.Name, string(data)) + } + }) + + return err +} + +// Parse parses all files in every folder under /run/secrets as if they were command-line flags. +// +// This is most useful when you are using environments like Kubernetes where the path of least resistance +// is to mount your secrets as a filesystem. Mount all your secrets into the pod and then let it figure +// itself out! +// +// To use this effectively, ensure that your Pods and Deployments mount secrets as volumes like this: +// +// volumes: +// - name: secret-volume +// secret: +// secretName: shell +// containers: +// - name: shell +// image: ubuntu:latest +// volumeMounts: +// - name: secret-volume +// readOnly: true +// mountPath: "/run/secrets/shell" +// +// By default this will attempt to correct for several styles of naming for files: +// +// * kebab-case (the default) +// * SHOUTING-KEBAB-CASE +// * snake_case +// * SHOUTING_SNAKE_CASE +// * camelCase +// * HammerCase +func Parse() { + stats, err := os.ReadDir("/run/secrets") + if err != nil { + slog.Debug("can't read from /run/secrets", "err", err) + return + } + + for _, stat := range stats { + if !stat.IsDir() { + continue + } + + loc := filepath.Join("/run/secrets", stat.Name()) + + if err := ParseSet(loc, flag.CommandLine); err != nil { + slog.Error("can't parse folder", "folder", loc, "err", err) + } + } +} diff --git a/flagfolder/flagfolder_test.go b/flagfolder/flagfolder_test.go new file mode 100644 index 0000000..d85e023 --- /dev/null +++ b/flagfolder/flagfolder_test.go @@ -0,0 +1,44 @@ +package flagfolder + +import ( + "flag" + "testing" +) + +func TestFlagFolderSimple(t *testing.T) { + for _, cs := range []struct { + flagName string + wantValue string + }{ + { + flagName: "foo", + wantValue: "foo", + }, + { + flagName: "bar", + wantValue: "bar", + }, + { + flagName: "something-here", + wantValue: "something here", + }, + { + flagName: "what-is-computer", + wantValue: "what is computer", + }, + } { + t.Run(cs.flagName, func(t *testing.T) { + fs := flag.NewFlagSet("flagfolder_test", flag.PanicOnError) + + f := fs.String(cs.flagName, "fail", "help for "+cs.flagName) + + if err := ParseSet("./testdata", fs); err != nil { + t.Errorf("can't parse ./testdata: %v", err) + } + + if *f != cs.wantValue { + t.Errorf("wanted --%s to be %q, got: %q", cs.flagName, cs.wantValue, *f) + } + }) + } +} diff --git a/flagfolder/testdata/FOO b/flagfolder/testdata/FOO new file mode 100644 index 0000000..1910281 --- /dev/null +++ b/flagfolder/testdata/FOO @@ -0,0 +1 @@ +foo
\ No newline at end of file diff --git a/flagfolder/testdata/WHAT_IS_COMPUTER b/flagfolder/testdata/WHAT_IS_COMPUTER new file mode 100644 index 0000000..aae2638 --- /dev/null +++ b/flagfolder/testdata/WHAT_IS_COMPUTER @@ -0,0 +1 @@ +what is computer
\ No newline at end of file diff --git a/flagfolder/testdata/bar b/flagfolder/testdata/bar new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/flagfolder/testdata/bar @@ -0,0 +1 @@ +bar
\ No newline at end of file diff --git a/flagfolder/testdata/something_here b/flagfolder/testdata/something_here new file mode 100644 index 0000000..709c317 --- /dev/null +++ b/flagfolder/testdata/something_here @@ -0,0 +1 @@ +something here
\ No newline at end of file diff --git a/flagfolder/testpod.yml b/flagfolder/testpod.yml new file mode 100644 index 0000000..392d629 --- /dev/null +++ b/flagfolder/testpod.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Secret +metadata: + name: shell + namespace: default +data: + foo: Zm9v + bar: YmFy +--- +apiVersion: v1 +kind: Pod +metadata: + name: shell + namespace: default +spec: + volumes: + - name: secret-volume + secret: + secretName: shell + containers: + - name: shell + image: ubuntu:latest + command: + - sleep + - "infinity" + imagePullPolicy: IfNotPresent + volumeMounts: + - name: secret-volume + readOnly: true + mountPath: "/run/secrets/shell" + restartPolicy: Always @@ -1,4 +1,4 @@ -# nix-direnv cache busting line: sha256-hbKmwwxDWQqwJpym2wFOTJCc5ByZTM7WBQM2l5yXuDw= +# nix-direnv cache busting line: sha256-YARBn+bGzZ3FNAfyMdsWFeRDiOKp9XShbtIIAFIchlM= { description = "/x/perimental code"; @@ -45,6 +45,7 @@ require ( github.com/rogpeppe/go-internal v1.12.0 github.com/rs/cors v1.11.0 github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a + github.com/stoewer/go-strcase v1.3.0 github.com/tetratelabs/wazero v1.7.0 github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef @@ -748,6 +748,8 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= diff --git a/internal/internal.go b/internal/internal.go index b9c1962..671360f 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -10,6 +10,7 @@ import ( "github.com/posener/complete" "go4.org/legal" + "within.website/x/flagfolder" "within.website/x/internal/confyg/flagconfyg" "within.website/x/internal/flagenv" "within.website/x/internal/manpage" @@ -48,14 +49,16 @@ func configFileLocation() string { // // - command line flags (to get -config) // - environment variables +// - any secrets mounted to /run/secrets // - configuration file (if -config is set) // - command line flags // // This is done this way to ensure that command line flags always are the deciding -// factor as an escape hatch. +// factor as an escape hatch, at the cost of potentially evaluating flags twice. func HandleStartup() { flag.Parse() flagenv.Parse() + flagfolder.Parse() ctx := context.Background() |
