diff --git a/cmd/generate.go b/cmd/generate.go index a6b1845d..a2d3ed4d 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -20,6 +20,7 @@ import ( func NewGenerateCommand() *cobra.Command { const StdIn = "-" var configPath, secretName string + var disableRecursive bool var verboseOutput bool var disableCache bool @@ -43,7 +44,7 @@ func NewGenerateCommand() *cobra.Command { return err } } else { - files, err := listFiles(path) + files, err := listFiles(path, disableRecursive) if len(files) < 1 { return fmt.Errorf("no YAML or JSON files were found in %s", path) } @@ -119,5 +120,6 @@ func NewGenerateCommand() *cobra.Command { command.Flags().StringVarP(&secretName, "secret-name", "s", "", "name of a Kubernetes Secret in the argocd namespace containing Vault configuration data in the argocd namespace of your ArgoCD host (Only available when used in ArgoCD). The namespace can be overridden by using the format :") command.Flags().BoolVar(&verboseOutput, "verbose-sensitive-output", false, "enable verbose mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr") command.Flags().BoolVar(&disableCache, "disable-token-cache", false, "disable the automatic token cache feature that store tokens locally") + command.Flags().BoolVar(&disableRecursive, "disable-recursive", false, "disable resursive path traversal") return command } diff --git a/cmd/generate_test.go b/cmd/generate_test.go index f428c690..f7a2643b 100644 --- a/cmd/generate_test.go +++ b/cmd/generate_test.go @@ -300,6 +300,71 @@ func TestMain(t *testing.T) { } }) + t.Run("will not recurse down directories if disabled", func(t *testing.T) { + args := []string{ + "--disable-recursive", + "../fixtures/input/nonempty-recursive", + } + cmd := NewGenerateCommand() + + b := bytes.NewBufferString("") + e := bytes.NewBufferString("") + cmd.SetArgs(args) + cmd.SetOut(b) + cmd.SetErr(e) + cmd.Execute() + out, err := io.ReadAll(b) // Read buffer to bytes + if err != nil { + t.Fatal(err) + } + stderr, err := io.ReadAll(e) // Read buffer to bytes + if err != nil { + t.Fatal(err) + } + + buf, err := os.ReadFile("../fixtures/output/secret-recursive-disabled.yaml") + if err != nil { + t.Fatal(err) + } + + expected := string(buf) + if string(out) != expected { + t.Fatalf("expected:\n\n%s\nbut got:\n\n%s\nerr: %s", expected, string(out), string(stderr)) + } + }) + + t.Run("will recurse down directories if unset", func(t *testing.T) { + args := []string{ + "../fixtures/input/nonempty-recursive", + } + cmd := NewGenerateCommand() + + b := bytes.NewBufferString("") + e := bytes.NewBufferString("") + cmd.SetArgs(args) + cmd.SetOut(b) + cmd.SetErr(e) + cmd.Execute() + out, err := io.ReadAll(b) // Read buffer to bytes + if err != nil { + t.Fatal(err) + } + stderr, err := io.ReadAll(e) // Read buffer to bytes + if err != nil { + t.Fatal(err) + } + + buf, err := os.ReadFile("../fixtures/output/secret-recursive-enabled.yaml") + if err != nil { + t.Fatal(err) + } + + expected := string(buf) + if string(out) != expected { + t.Fatalf("expected:\n\n%s\nbut got:\n\n%s\nerr: %s", expected, string(out), string(stderr)) + } + }) + os.Unsetenv("AVP_TYPE") os.Unsetenv("VAULT_ADDR") os.Unsetenv("AVP_AUTH_TYPE") diff --git a/cmd/util.go b/cmd/util.go index 7ef26ad4..19eed20d 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -11,15 +11,26 @@ import ( k8yaml "k8s.io/apimachinery/pkg/util/yaml" ) -func listFiles(root string) ([]string, error) { +func listFiles(root string, disableRecursive bool) ([]string, error) { var files []string - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error accessing path %s: %w", path, err) + } + if info.IsDir() { + if disableRecursive && path != root { + return filepath.SkipDir + } + return nil + } if filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".json" { files = append(files, path) } return nil - }) + } + + err := filepath.Walk(root, walkFn) if err != nil { return files, err } diff --git a/cmd/util_test.go b/cmd/util_test.go new file mode 100644 index 00000000..7da0003d --- /dev/null +++ b/cmd/util_test.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "os" + "path/filepath" + "reflect" + "sort" + "testing" +) + +// Helper to create files and directories for testing +func createTestFiles(t *testing.T, root string, files map[string]string) { + for path, content := range files { + fullPath := filepath.Join(root, path) + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("failed to create dir %s: %v", dir, err) + } + if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil { + t.Fatalf("failed to write file %s: %v", fullPath, err) + } + } +} + +func TestListFiles_NonRecursive(t *testing.T) { + tmpDir := t.TempDir() + files := map[string]string{ + "file1.yaml": "a: b", + "file2.yml": "c: d", + "file3.json": "{}", + "file4.txt": "not yaml", + "subdir/file5.yaml": "e: f", + } + createTestFiles(t, tmpDir, files) + + got, err := listFiles(tmpDir, true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := []string{ + filepath.Join(tmpDir, "file1.yaml"), + filepath.Join(tmpDir, "file2.yml"), + filepath.Join(tmpDir, "file3.json"), + } + sort.Strings(got) + sort.Strings(want) + if !reflect.DeepEqual(got, want) { + t.Errorf("expected %v, got %v", want, got) + } +} + +func TestListFiles_Recursive(t *testing.T) { + tmpDir := t.TempDir() + files := map[string]string{ + "file1.yaml": "a: b", + "file2.yml": "c: d", + "file3.json": "{}", + "file4.txt": "not yaml", + "subdir/file5.yaml": "e: f", + "subdir/file6.json": "{}", + "subdir2/file7.yml": "g: h", + "subdir2/file8.txt": "not yaml", + } + createTestFiles(t, tmpDir, files) + + got, err := listFiles(tmpDir, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := []string{ + filepath.Join(tmpDir, "file1.yaml"), + filepath.Join(tmpDir, "file2.yml"), + filepath.Join(tmpDir, "file3.json"), + filepath.Join(tmpDir, "subdir", "file5.yaml"), + filepath.Join(tmpDir, "subdir", "file6.json"), + filepath.Join(tmpDir, "subdir2", "file7.yml"), + } + sort.Strings(got) + sort.Strings(want) + if !reflect.DeepEqual(got, want) { + t.Errorf("expected %v, got %v", want, got) + } +} + +func TestListFiles_Error(t *testing.T) { + // Non-existent directory should return an error + _, err := listFiles("/non/existent/dir", false) + if err == nil { + t.Error("expected error for non-existent directory, got nil") + } +} diff --git a/docs/cmd/generate.md b/docs/cmd/generate.md index ade76457..f7a2499b 100644 --- a/docs/cmd/generate.md +++ b/docs/cmd/generate.md @@ -7,6 +7,8 @@ argocd-vault-plugin generate PATH [flags] ### Options ``` -c, --config-path string path to a file containing Vault configuration (YAML, JSON, envfile) to use + --disable-recursive disable resursive path traversal + --disable-token-cache disable the automatic token cache feature that store tokens locally -h, --help help for generate -s, --secret-name string name of a Kubernetes Secret in the argocd namespace containing Vault configuration data in the argocd namespace of your ArgoCD host (Only available when used in ArgoCD). The namespace can be overridden by using the format : --verbose-sensitive-output enable verbose mode for detailed info to help with debugging. Includes sensitive data (credentials), logged to stderr diff --git a/fixtures/output/secret-recursive-disabled.yaml b/fixtures/output/secret-recursive-disabled.yaml new file mode 100644 index 00000000..ac637de3 --- /dev/null +++ b/fixtures/output/secret-recursive-disabled.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +data: + SECRET_NUM: MQ== + SECRET_VAR: dGVzdC1wYXNzd29yZA== +kind: Secret +metadata: + annotations: + avp.kubernetes.io/kv-version: "1" + avp.kubernetes.io/path: secret/testing + name: test-name + namespace: test-namespace +type: Opaque +--- diff --git a/fixtures/output/secret-recursive-enabled.yaml b/fixtures/output/secret-recursive-enabled.yaml new file mode 100644 index 00000000..466f951d --- /dev/null +++ b/fixtures/output/secret-recursive-enabled.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +data: + SECRET_NUM: MQ== + SECRET_VAR: dGVzdC1wYXNzd29yZA== +kind: Secret +metadata: + annotations: + avp.kubernetes.io/kv-version: "1" + avp.kubernetes.io/path: secret/testing + name: test-name + namespace: test-namespace +type: Opaque +--- +apiVersion: v1 +data: + SECRET_NUM: MQ== + SECRET_VAR: dGVzdC1wYXNzd29yZA== +kind: Secret +metadata: + annotations: + avp.kubernetes.io/kv-version: "1" + avp.kubernetes.io/path: secret/testing + name: test-name + namespace: test-namespace +type: Opaque +---