From 632af1a2be3e4a58fc91658641e6c939e1543289 Mon Sep 17 00:00:00 2001 From: Roberto Hidalgo Date: Tue, 10 Jan 2023 22:50:06 -0600 Subject: [PATCH] test about half of what matters --- .milpa/commands/joao/test/unit.sh | 19 ++ .milpa/commands/joao/test/unit.yaml | 13 + bad-test.yaml | 5 + cmd/fetch_test.go | 105 +++++++ cmd/get.go | 21 +- cmd/get_test.go | 331 +++++++++++++++++--- cmd/set.go | 10 +- cmd/set_test.go | 457 ++++++++++++++++++++++++++++ cmd/vault-plugin.go | 2 +- internal/op-client/connect.go | 27 +- pkg/config/config.go | 21 +- pkg/config/entry.go | 51 ++-- pkg/config/input.go | 8 +- pkg/config/lookup.go | 18 +- pkg/config/output.go | 2 +- test.yaml | 6 +- 16 files changed, 989 insertions(+), 107 deletions(-) create mode 100644 .milpa/commands/joao/test/unit.sh create mode 100644 .milpa/commands/joao/test/unit.yaml create mode 100644 bad-test.yaml create mode 100644 cmd/fetch_test.go create mode 100644 cmd/set_test.go diff --git a/.milpa/commands/joao/test/unit.sh b/.milpa/commands/joao/test/unit.sh new file mode 100644 index 0000000..f0e144d --- /dev/null +++ b/.milpa/commands/joao/test/unit.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2021 Roberto Hidalgo + +cd "$MILPA_REPO_ROOT" || @milpa.fail "could not cd into $MILPA_REPO_ROOT" +@milpa.log info "Running unit tests" +args=() +after_run=complete +if [[ "${MILPA_OPT_COVERAGE}" ]]; then + after_run=success + args=( -coverprofile=coverage.out --coverpkg=./...) +fi +gotestsum --format testname -- "$MILPA_ARG_SPEC" "${args[@]}" || exit 2 +@milpa.log "$after_run" "Unit tests passed" + +[[ ! "${MILPA_OPT_COVERAGE}" ]] && exit +@milpa.log info "Building coverage report" +go tool cover -html=coverage.out -o coverage.html || @milpa.fail "could not build reports" +@milpa.log complete "Coverage report ready at coverage.html" diff --git a/.milpa/commands/joao/test/unit.yaml b/.milpa/commands/joao/test/unit.yaml new file mode 100644 index 0000000..60d3dbd --- /dev/null +++ b/.milpa/commands/joao/test/unit.yaml @@ -0,0 +1,13 @@ +summary: Runs unit tests +description: | + Runs unit tests using gotestsum +arguments: + - name: spec + default: ./... + description: the package to test + values: + dirs: "*" +options: + coverage: + type: bool + description: if provided, will output coverage reports diff --git a/bad-test.yaml b/bad-test.yaml new file mode 100644 index 0000000..60e3f91 --- /dev/null +++ b/bad-test.yaml @@ -0,0 +1,5 @@ +_config: !!joao + name: some:test + vault: bad-example + int: -:a\ + diff --git a/cmd/fetch_test.go b/cmd/fetch_test.go new file mode 100644 index 0000000..17ccb0d --- /dev/null +++ b/cmd/fetch_test.go @@ -0,0 +1,105 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package cmd_test + +import ( + "bytes" + "strings" + "testing" + + . "git.rob.mx/nidito/joao/cmd" + "git.rob.mx/nidito/joao/internal/op-client/mock" + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func TestFetch(t *testing.T) { + mockOPConnect(t) + f := testConfig.Fields + s := testConfig.Sections + defer func() { testConfig.Fields = f; testConfig.Sections = s }() + + testConfig.Sections = append(testConfig.Sections, + &onepassword.ItemSection{ID: "o", Label: "o"}, + &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"}, + ) + testConfig.Fields = append(testConfig.Fields, + &onepassword.ItemField{ + ID: "o.ganso.gosto", + Section: &onepassword.ItemSection{ID: "o", Label: "o"}, + Type: "STRING", + Label: "ganso.gosto", + Value: "da dupla", + }, + &onepassword.ItemField{ + ID: "e-fez-tambem.0", + Section: &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"}, + Type: "STRING", + Label: "0", + Value: "quém!", + }, + &onepassword.ItemField{ + ID: "e-fez-tambem.1", + Section: &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"}, + Type: "STRING", + Label: "1", + Value: "quém!", + }, + &onepassword.ItemField{ + ID: "e-fez-tambem.2", + Section: &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"}, + Type: "STRING", + Label: "2", + Value: "quém!", + }) + mock.Update(testConfig) + root := fromProjectRoot() + out := bytes.Buffer{} + Fetch.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("dry-run", true, "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Fetch.Cobra = cmd + logrus.SetLevel(logrus.DebugLevel) + err := Fetch.Run(cmd, []string{root + "/test.yaml"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `_config: !!joao + name: some:test + vault: example +# not sorted on purpose +int: 1 # line +# foot +string: pato +bool: false +secret: !!secret very secret +nested: + string: quem + int: 1 + secret: !!secret very secret + bool: true + list: + - 1 + - 2 + - 3 +list: + - one + - two + - three +o: + ganso: + gosto: da dupla +e-fez-tambem: + - quém! + - quém! + - quém!` + + if got := out.String(); strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} diff --git a/cmd/get.go b/cmd/get.go index 50b5649..5be9246 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -77,6 +77,7 @@ looks at the filesystem or remotely, using 1password (over the CLI if available, } if query == "" || query == "." { + var bytes []byte switch format { case "yaml", "raw", "diff-yaml": modes := []config.OutputMode{} @@ -86,21 +87,17 @@ looks at the filesystem or remotely, using 1password (over the CLI if available, if format == "diff-yaml" { modes = append(modes, config.OutputModeNoComments, config.OutputModeSorted) } - bytes, err := cfg.AsYAML(modes...) - if err != nil { - return err - } - _, err = cmd.Cobra.OutOrStdout().Write(bytes) - return err + bytes, err = cfg.AsYAML(modes...) case "json", "op": - bytes, err := cfg.AsJSON(redacted, format == "op") - if err != nil { - return err - } - _, err = cmd.Cobra.OutOrStdout().Write(bytes) + bytes, err = cfg.AsJSON(redacted, format == "op") + default: + return fmt.Errorf("unknown format %s", format) + } + if err != nil { return err } - return fmt.Errorf("unknown format %s", format) + _, err = cmd.Cobra.OutOrStdout().Write(bytes) + return err } parts := strings.Split(query, ".") diff --git a/cmd/get_test.go b/cmd/get_test.go index de0b80e..be18953 100644 --- a/cmd/get_test.go +++ b/cmd/get_test.go @@ -11,9 +11,166 @@ import ( "testing" . "git.rob.mx/nidito/joao/cmd" + opclient "git.rob.mx/nidito/joao/internal/op-client" + "git.rob.mx/nidito/joao/internal/op-client/mock" + "github.com/1Password/connect-sdk-go/connect" + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +var testConfig = &onepassword.Item{ + Title: "some:test", + Vault: onepassword.ItemVault{ID: "example"}, + Category: "PASSWORD", + Sections: []*onepassword.ItemSection{ + {ID: "~annotations", Label: "~annotations"}, + // {ID: "nested", Label: "nested"}, + {ID: "list", Label: "list"}, + }, + Fields: []*onepassword.ItemField{ + { + ID: "password", + Type: "CONCEALED", + Purpose: "PASSWORD", + Label: "password", + Value: "56615e9be5f0ce5f97d5b446faaa1d39f95a13a1ea8326ae933c3d29eb29735c", + }, + { + ID: "notesPlain", + Type: "STRING", + Purpose: "NOTES", + Label: "notesPlain", + Value: "flushed by joao", + }, + { + ID: "~annotations.int", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "int", + Value: "int", + }, + { + ID: "int", + Type: "STRING", + Label: "int", + Value: "1", + }, + { + ID: "string", + Type: "STRING", + Label: "string", + Value: "pato", + }, + { + ID: "~annotations.bool", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "bool", + Value: "bool", + }, + { + ID: "bool", + Type: "STRING", + Label: "bool", + Value: "false", + }, + { + ID: "~annotations.secret", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "secret", + Value: "secret", + }, + { + ID: "secret", + Type: "CONCEALED", + Label: "secret", + Value: "very secret", + }, + { + ID: "nested.string", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Type: "STRING", + Label: "string", + Value: "quem", + }, + { + ID: "~annotations.nested.int", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "nested.int", + Value: "int", + }, + { + ID: "nested.int", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Type: "STRING", + Label: "int", + Value: "1", + }, + { + ID: "~annotations.nested.secret", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "nested.secret", + Value: "secret", + }, + { + ID: "nested.secret", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Type: "CONCEALED", + Label: "secret", + Value: "very secret", + }, + { + ID: "~annotations.nested.bool", + Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"}, + Type: "STRING", + Label: "nested.bool", + Value: "bool", + }, + { + ID: "nested.bool", + Section: &onepassword.ItemSection{ID: "nested", Label: "nested"}, + Type: "STRING", + Label: "bool", + Value: "true", + }, + { + ID: "list.0", + Section: &onepassword.ItemSection{ID: "list", Label: "list"}, + Type: "STRING", + Label: "0", + Value: "one", + }, + { + ID: "list.1", + Section: &onepassword.ItemSection{ID: "list", Label: "list"}, + Type: "STRING", + Label: "1", + Value: "two", + }, + { + ID: "list.2", + Section: &onepassword.ItemSection{ID: "list", Label: "list"}, + Type: "STRING", + Label: "2", + Value: "three", + }, + }, +} + +func mockOPConnect(t *testing.T) { + t.Helper() + opclient.ConnectClientFactory = func(host, token, userAgent string) connect.Client { + return &mock.Client{} + } + client := opclient.NewConnect("", "") + opclient.Use(client) + mock.Add(testConfig) +} + func fromProjectRoot() string { _, filename, _, _ := runtime.Caller(0) dir := path.Join(path.Dir(filename), "../") @@ -24,29 +181,42 @@ func fromProjectRoot() string { return wd } -func TestGetRedacted(t *testing.T) { +func TestGetBadYAML(t *testing.T) { root := fromProjectRoot() - out := bytes.Buffer{} Get.SetBindings() + out := bytes.Buffer{} cmd := &cobra.Command{} - cmd.Flags().Bool("redacted", true, "") cmd.SetOut(&out) cmd.SetErr(&out) Get.Cobra = cmd - err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--redacted"}) - - if err != nil { - t.Fatalf("could not get: %s", err) + err := Get.Run(cmd, []string{root + "/bad-test.yaml", "."}) + if err == nil { + t.Fatalf("Did not throw on bad path: %s", out.String()) } - - expected, err := os.ReadFile(root + "/test.yaml") - if err != nil { - t.Fatalf("could not read file: %s", err) + wantedPrefix := "could not parse file" + wantedSuffix := "/bad-test.yaml as yaml: line 4: mapping values are not allowed in this context" + if got := err.Error(); !(strings.HasPrefix(got, wantedPrefix) && strings.HasSuffix(got, wantedSuffix)) { + t.Fatalf("Failed with bad error, wanted %s /some-path%s, got %s", wantedPrefix, wantedSuffix, got) } +} - got := out.String() - if strings.TrimSpace(got) != strings.ReplaceAll(strings.TrimSpace(string(expected)), " very secret", "") { - t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) +func TestGetBadPath(t *testing.T) { + root := fromProjectRoot() + Get.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/does-not-exist.yaml", "."}) + if err == nil { + t.Fatalf("Did not throw on bad path: %s", out.String()) + } + wantedPrefix := "could not read file" + wantedSuffix := "/does-not-exist.yaml" + + if got := err.Error(); !(strings.HasPrefix(got, wantedPrefix) && strings.HasSuffix(got, wantedSuffix)) { + t.Fatalf("Failed with bad error, wanted %s /some-path%s, got %s", wantedPrefix, wantedSuffix, got) } } @@ -70,8 +240,32 @@ func TestGetNormal(t *testing.T) { t.Fatalf("could not read file: %s", err) } - got := out.String() - if strings.TrimSpace(got) != strings.TrimSpace(string(expected)) { + if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(string(expected)) { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetRedacted(t *testing.T) { + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", true, "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--redacted"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected, err := os.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file: %s", err) + } + + if got := out.String(); strings.TrimSpace(got) != strings.ReplaceAll(strings.TrimSpace(string(expected)), " very secret", "") { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -82,6 +276,7 @@ func TestGetPath(t *testing.T) { Get.SetBindings() cmd := &cobra.Command{} cmd.Flags().Bool("redacted", false, "") + cmd.Flags().StringP("output", "o", "yaml", "") cmd.SetOut(&out) cmd.SetErr(&out) Get.Cobra = cmd @@ -92,8 +287,29 @@ func TestGetPath(t *testing.T) { } expected := "very secret" - got := out.String() - if strings.TrimSpace(got) != strings.TrimSpace(expected) { + if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) { + t.Fatalf("did not get expected scalar output:\nwanted: %s\ngot: %s", expected, got) + } + + out = bytes.Buffer{} + cmd.SetOut(&out) + cmd.SetErr(&out) + err = Get.Run(cmd, []string{root + "/test.yaml", "nested", "--output", "diff-yaml"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected = `bool: true +int: 1 +list: + - 1 + - 2 + - 3 +secret: very secret +string: quem` + + if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -116,11 +332,14 @@ func TestGetPathCollection(t *testing.T) { expected := `bool: true int: 1 +list: + - 1 + - 2 + - 3 secret: very secret string: quem` - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -153,13 +372,16 @@ list: nested: bool: true int: 1 + list: + - 1 + - 2 + - 3 secret: !!secret very secret string: quem secret: !!secret very secret -string: "pato"` +string: pato` - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -180,10 +402,9 @@ func TestGetJSON(t *testing.T) { t.Fatalf("could not get: %s", err) } - expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"secret":"very secret","string":"quem"},"secret":"very secret","string":"pato"}` + expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"list":[1,2,3],"secret":"very secret","string":"quem"},"secret":"very secret","string":"pato"}` - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -204,10 +425,9 @@ func TestGetJSONPathScalar(t *testing.T) { t.Fatalf("could not get: %s", err) } - expected := `very secret` + expected := `very secret` // nolint: ifshort - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -228,10 +448,9 @@ func TestGetJSONPathCollection(t *testing.T) { t.Fatalf("could not get: %s", err) } - expected := `{"bool":true,"int":1,"secret":"very secret","string":"quem"}` + expected := `{"bool":true,"int":1,"list":[1,2,3],"secret":"very secret","string":"quem"}` - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -252,10 +471,9 @@ func TestGetJSONRedacted(t *testing.T) { t.Fatalf("could not get: %s", err) } - expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"secret":"","string":"quem"},"secret":"","string":"pato"}` + expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"list":[1,2,3],"secret":"","string":"quem"},"secret":"","string":"pato"}` - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } @@ -275,10 +493,47 @@ func TestGetJSONOP(t *testing.T) { t.Fatalf("could not get: %s", err) } - expected := `{"id":"","title":"some:test","vault":{"id":"example"},"category":"PASSWORD","sections":[{"id":"~annotations","label":"~annotations"},{"id":"nested","label":"nested"}],"fields":[{"id":"password","type":"CONCEALED","purpose":"PASSWORD","label":"password","value":"56615e9be5f0ce5f97d5b446faaa1d39f95a13a1ea8326ae933c3d29eb29735c"},{"id":"notesPlain","type":"STRING","purpose":"NOTES","label":"notesPlain","value":"flushed by joao"},{"id":"~annotations.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"int","value":"int"},{"id":"int","type":"STRING","label":"int","value":"1"},{"id":"string","type":"STRING","label":"string","value":"pato"},{"id":"~annotations.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"bool","value":"bool"},{"id":"bool","type":"STRING","label":"bool","value":"false"},{"id":"~annotations.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"secret","value":"secret"},{"id":"secret","type":"CONCEALED","label":"secret","value":"very secret"},{"id":"nested.string","section":{"id":"nested"},"type":"STRING","label":"string","value":"quem"},{"id":"~annotations.nested.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.int","value":"int"},{"id":"nested.int","section":{"id":"nested"},"type":"STRING","label":"int","value":"1"},{"id":"~annotations.nested.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.secret","value":"secret"},{"id":"nested.secret","section":{"id":"nested"},"type":"CONCEALED","label":"secret","value":"very secret"},{"id":"~annotations.nested.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.bool","value":"bool"},{"id":"nested.bool","section":{"id":"nested"},"type":"STRING","label":"bool","value":"true"},{"id":"list.0","section":{"id":"list"},"type":"STRING","label":"0","value":"one"},{"id":"list.1","section":{"id":"list"},"type":"STRING","label":"1","value":"two"},{"id":"list.2","section":{"id":"list"},"type":"STRING","label":"2","value":"three"}],"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}` + expected := `{"id":"","title":"some:test","vault":{"id":"example"},"category":"PASSWORD","sections":[{"id":"~annotations","label":"~annotations"},{"id":"nested","label":"nested"},{"id":"list","label":"list"}],"fields":[{"id":"password","type":"CONCEALED","purpose":"PASSWORD","label":"password","value":"cedbdf86fb15cf1237569e9b3188372d623aea9d6a707401aca656645590227c"},{"id":"notesPlain","type":"STRING","purpose":"NOTES","label":"notesPlain","value":"flushed by joao"},{"id":"~annotations.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"int","value":"int"},{"id":"int","type":"STRING","label":"int","value":"1"},{"id":"string","type":"STRING","label":"string","value":"pato"},{"id":"~annotations.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"bool","value":"bool"},{"id":"bool","type":"STRING","label":"bool","value":"false"},{"id":"~annotations.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"secret","value":"secret"},{"id":"secret","type":"CONCEALED","label":"secret","value":"very secret"},{"id":"nested.string","section":{"id":"nested"},"type":"STRING","label":"string","value":"quem"},{"id":"~annotations.nested.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.int","value":"int"},{"id":"nested.int","section":{"id":"nested"},"type":"STRING","label":"int","value":"1"},{"id":"~annotations.nested.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.secret","value":"secret"},{"id":"nested.secret","section":{"id":"nested"},"type":"CONCEALED","label":"secret","value":"very secret"},{"id":"~annotations.nested.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.bool","value":"bool"},{"id":"nested.bool","section":{"id":"nested"},"type":"STRING","label":"bool","value":"true"},{"id":"~annotations.nested.list.0","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.list.0","value":"int"},{"id":"nested.list.0","section":{"id":"nested"},"type":"STRING","label":"list.0","value":"1"},{"id":"~annotations.nested.list.1","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.list.1","value":"int"},{"id":"nested.list.1","section":{"id":"nested"},"type":"STRING","label":"list.1","value":"2"},{"id":"~annotations.nested.list.2","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.list.2","value":"int"},{"id":"nested.list.2","section":{"id":"nested"},"type":"STRING","label":"list.2","value":"3"},{"id":"list.0","section":{"id":"list"},"type":"STRING","label":"0","value":"one"},{"id":"list.1","section":{"id":"list"},"type":"STRING","label":"1","value":"two"},{"id":"list.2","section":{"id":"list"},"type":"STRING","label":"2","value":"three"}],"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}` - got := out.String() - if strings.TrimSpace(got) != expected { + if got := out.String(); strings.TrimSpace(got) != expected { + t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) + } +} + +func TestGetRemote(t *testing.T) { + mockOPConnect(t) + root := fromProjectRoot() + out := bytes.Buffer{} + Get.SetBindings() + cmd := &cobra.Command{} + cmd.Flags().Bool("redacted", false, "") + cmd.Flags().Bool("remote", true, "") + cmd.Flags().StringP("output", "o", "diff-yaml", "") + cmd.SetOut(&out) + cmd.SetErr(&out) + Get.Cobra = cmd + logrus.SetLevel(logrus.DebugLevel) + err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--output", "diff-yaml", "--remote"}) + + if err != nil { + t.Fatalf("could not get: %s", err) + } + + expected := `bool: false +int: 1 +list: + - one + - two + - three +nested: + bool: true + int: 1 + secret: !!secret very secret + string: quem +secret: !!secret very secret +string: pato` + + if got := out.String(); strings.TrimSpace(got) != expected { t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) } } diff --git a/cmd/set.go b/cmd/set.go index a6d708d..a82b550 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -5,6 +5,7 @@ package cmd import ( "fmt" "io/fs" + "io/ioutil" "os" "strings" @@ -89,7 +90,7 @@ Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH } if delete && input != "/dev/stdin" { - logrus.Warn("Ignoring --file while deleting") + logrus.Warn("Ignoring --input while deleting") } cfg, err = config.Load(path, false) @@ -104,7 +105,12 @@ Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH return err } } else { - valueBytes, err := os.ReadFile(input) + var valueBytes []byte + if input == "/dev/stdin" { + valueBytes, err = ioutil.ReadAll(cmd.Cobra.InOrStdin()) + } else { + valueBytes, err = os.ReadFile(input) + } if err != nil { return err } diff --git a/cmd/set_test.go b/cmd/set_test.go new file mode 100644 index 0000000..9637415 --- /dev/null +++ b/cmd/set_test.go @@ -0,0 +1,457 @@ +// Copyright © 2022 Roberto Hidalgo +// SPDX-License-Identifier: Apache-2.0 +package cmd_test + +import ( + "bytes" + "fmt" + "io/fs" + "io/ioutil" + "os" + "strings" + "testing" + + . "git.rob.mx/nidito/joao/cmd" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func tempTestYaml(root, name string, data []byte) (string, func(), error) { + path := fmt.Sprintf("%s/test-%s.yaml", root, name) + if err := ioutil.WriteFile(path, data, fs.FileMode(0644)); err != nil { + return path, nil, fmt.Errorf("could not create test file") + } + return path, func() { os.Remove(path) }, nil +} + +func TestSet(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + stdin := bytes.Buffer{} + stdin.Write([]byte("pato\nganso\nmarreco\n")) + cmd.SetIn(&stdin) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-plain", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "string"}) + if err != nil { + t.Fatalf("Threw on good set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), ` +string: |- + pato + ganso + marreco`) { + t.Fatalf("Did not contain expected new string, got:\n%s", changed) + } +} + +func TestSetSecret(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + stdin := bytes.Buffer{} + stdin.Write([]byte("new secret\n")) + cmd.SetIn(&stdin) + Set.Cobra = cmd + cmd.Flags().Bool("secret", true, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-plain", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "secret", "--secret"}) + if err != nil { + t.Fatalf("Threw on good set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), "\nsecret: !!secret new secret\n") { + t.Fatalf("Did not contain expected new string, got:\n%s", changed) + } +} + +func TestSetFromFile(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + dataPath, dataCleanup, err := tempTestYaml(root, "set-from-file-data", []byte("ganso")) + if err != nil { + t.Fatal(err) + } + defer dataCleanup() + cmd.Flags().StringP("input", "i", dataPath, "") + + path, cleanup, err := tempTestYaml(root, "set-from-file", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "string", "--input", dataPath}) + if err != nil { + t.Fatalf("Threw on good set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), "\nstring: ganso\n") { + t.Fatalf("Did not contain expected new string, got:\n%s", changed) + } +} + +func TestSetNew(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + stdin := bytes.Buffer{} + stdin.Write([]byte("pato\nganso\nmarreco\ncisne\n")) + cmd.SetIn(&stdin) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + cmd.Flags().StringP("input", "i", "/dev/stdin", "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-new-key", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "quarteto"}) + if err != nil { + t.Fatalf("Threw on good new set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), ` +quarteto: |- + pato + ganso + marreco + cisne`) { + t.Fatalf("Did not contain expected new string, got:\n%s", changed) + } +} + +func TestSetNested(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + stdin := bytes.Buffer{} + stdin.Write([]byte("tico")) + cmd.SetIn(&stdin) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + cmd.Flags().StringP("input", "i", "/dev/stdin", "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-nested-key", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "nested.tico"}) + if err != nil { + t.Fatalf("Threw on good nested set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), ` + tico: tico +`) { + t.Fatalf("Did not contain expected new string, got:\n%s", changed) + } +} + +func TestSetJSON(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + stdin := bytes.Buffer{} + stdin.Write([]byte(`{"foram": "ensaiar", "para": "começar"}`)) + cmd.SetIn(&stdin) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", true, "") + cmd.Flags().Bool("flush", false, "") + cmd.Flags().StringP("input", "i", "/dev/stdin", "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-json", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "na-beira-da-lagoa"}) + if err != nil { + t.Fatalf("Threw on good nested set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), ` +na-beira-da-lagoa: + foram: ensaiar + para: começar +`) { + t.Fatalf("Did not contain expected new entry tree, got:\n%s", changed) + } +} + +func TestSetList(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + stdin := bytes.Buffer{} + stdin.Write([]byte("um")) + cmd.SetIn(&stdin) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", false, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + cmd.Flags().StringP("input", "i", "/dev/stdin", "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-list-key", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "asdf.0"}) + if err != nil { + t.Fatalf("Threw on good nested set: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if !strings.Contains(string(changed), ` +asdf: + - um +`) { + t.Fatalf("Did not contain expected new string, got:\n%s", changed) + } +} + +func TestDelete(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", true, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + cmd.Flags().StringP("input", "i", "/dev/stdin", "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-delete-key", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "string", "--delete"}) + if err != nil { + t.Fatalf("Threw on good set delete: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if strings.Contains(string(changed), ` +string: pato +`) { + t.Fatalf("Still contains deleted key, got:\n%s", changed) + } +} + +func TestDeleteNested(t *testing.T) { + root := fromProjectRoot() + Set.SetBindings() + out := bytes.Buffer{} + cmd := &cobra.Command{} + cmd.SetOut(&out) + cmd.SetErr(&out) + Set.Cobra = cmd + cmd.Flags().Bool("secret", false, "") + cmd.Flags().Bool("delete", true, "") + cmd.Flags().Bool("json", false, "") + cmd.Flags().Bool("flush", false, "") + cmd.Flags().StringP("input", "i", "/dev/stdin", "") + original, err := ioutil.ReadFile(root + "/test.yaml") + if err != nil { + t.Fatalf("could not read file") + } + + path, cleanup, err := tempTestYaml(root, "set-delete-nested-key", original) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + err = Set.Run(cmd, []string{path, "nested.string", "--delete"}) + if err != nil { + t.Fatalf("Threw on good set delete nested: %s", err) + } + + changed, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("could not read file") + } + + if string(changed) == string(original) { + t.Fatal("Did not change file") + } + + if strings.Contains(string(changed), ` + string: quem +`) { + t.Fatalf("Still contains deleted nested key, got:\n%s", changed) + } +} diff --git a/cmd/vault-plugin.go b/cmd/vault-plugin.go index 9bec0fb..daabc6f 100644 --- a/cmd/vault-plugin.go +++ b/cmd/vault-plugin.go @@ -12,7 +12,7 @@ import ( var Plugin = &command.Command{ Path: []string{"vault", "server"}, Summary: "Starts a vault-joao-plugin server", - Description: `Runs ﹅joao﹅ as a vault plugin. + Description: `Runs ﹅joao﹅ as a vault plugin. See https://developer.hashicorp.com/vault/docs/plugins You'll need to install ﹅joao﹅ in the machine running ﹅vault﹅ to ﹅plugin_directory﹅ as specified by vault's config. The installed ﹅joao﹅ executable needs to be executable for the user running vault only. diff --git a/internal/op-client/connect.go b/internal/op-client/connect.go index 9e3e031..4bc942c 100644 --- a/internal/op-client/connect.go +++ b/internal/op-client/connect.go @@ -3,10 +3,14 @@ package opclient import ( + "strings" + "github.com/1Password/connect-sdk-go/connect" op "github.com/1Password/connect-sdk-go/onepassword" ) +var ConnectClientFactory func(host, token, userAgent string) connect.Client = connect.NewClientWithUserAgent + // UUIDLength defines the required length of UUIDs. const UUIDLength = 26 @@ -33,7 +37,7 @@ type Connect struct { const userAgent = "nidito-joao" func NewConnect(host, token string) *Connect { - client := connect.NewClientWithUserAgent(host, token, userAgent) + client := ConnectClientFactory(host, token, userAgent) return &Connect{client: client} } @@ -41,12 +45,27 @@ func (b *Connect) Get(vault, name string) (*op.Item, error) { return b.client.GetItem(name, vault) } -func (b *Connect) Update(item *op.Item) error { +func (b *Connect) Update(item *op.Item, remote *op.Item) error { _, err := b.client.UpdateItem(item, item.Vault.ID) return err } func (b *Connect) List(vault, prefix string) ([]string, error) { - // TODO: get this done - return nil, nil + items, err := b.client.GetItems(vault) + if err != nil { + return nil, err + } + res := []string{} + for _, item := range items { + if prefix != "" && !strings.HasPrefix(item.Title, prefix) { + continue + } + res = append(res, item.Title) + } + return res, nil +} + +func (b *Connect) Create(item *op.Item) error { + _, err := b.client.CreateItem(item, item.Vault.ID) + return err } diff --git a/pkg/config/config.go b/pkg/config/config.go index a02b248..cc978ac 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -89,6 +89,12 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er if err := yaml.Unmarshal(data, newEntry); err != nil { return err } + if newEntry.Kind == yaml.MappingNode || newEntry.Kind == yaml.SequenceNode { + newEntry.Style = yaml.FoldedStyle | yaml.LiteralStyle + for _, v := range newEntry.Content { + v.Style = yaml.FlowStyle + } + } } else { valueStr = strings.Trim(valueStr, "\n") if isSecret { @@ -108,12 +114,14 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er entry := cfg.Tree for idx, key := range path { if len(path)-1 == idx { - dst := entry.ChildNamed(key) - if dst == nil { + if dst := entry.ChildNamed(key); dst == nil { + key := NewEntry(key, yaml.ScalarNode) if entry.Kind == yaml.MappingNode { - key := NewEntry(key, yaml.ScalarNode) + logrus.Infof("setting new map key %v", newEntry.Path) entry.Content = append(entry.Content, key, newEntry) } else { + logrus.Infof("setting new list key %v", newEntry.Path) + entry.Kind = yaml.SequenceNode entry.Content = append(entry.Content, newEntry) } } else { @@ -131,12 +139,15 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er } kind := yaml.MappingNode - if isNumeric(key) { + if idx+1 == len(path)-1 && isNumeric(path[idx+1]) { kind = yaml.SequenceNode } sub := NewEntry(key, kind) sub.Path = append(entry.Path, key) // nolint: gocritic - entry.Content = append(entry.Content, sub) + + keyEntry := NewEntry(sub.Name(), yaml.ScalarNode) + keyEntry.Value = key + entry.Content = append(entry.Content, keyEntry, sub) entry = sub } diff --git a/pkg/config/entry.go b/pkg/config/entry.go index c0f99ff..cef74cf 100644 --- a/pkg/config/entry.go +++ b/pkg/config/entry.go @@ -206,6 +206,10 @@ func (e *Entry) MarshalYAML() (*yaml.Node, error) { } } else { entries := e.Contents() + if len(entries)%2 != 0 { + return nil, fmt.Errorf("cannot decode odd numbered contents list: %s", e.Path) + } + for i := 0; i < len(entries); i += 2 { key := entries[i] value := entries[i+1] @@ -213,6 +217,11 @@ func (e *Entry) MarshalYAML() (*yaml.Node, error) { continue } + if key.Type == "" { + key.Kind = yaml.ScalarNode + key.Type = "!!map" + } + keyNode, err := key.MarshalYAML() if err != nil { return nil, err @@ -288,11 +297,11 @@ func (e *Entry) FromOP(fields []*op.ItemField) error { Type: kind, } if isNumeric(key) { - // logrus.Debugf("hydrating sequence value at %s", path) + logrus.Debugf("hydrating sequence value at %s", path) container.Kind = yaml.SequenceNode container.Content = append(container.Content, newEntry) } else { - // logrus.Debugf("hydrating map value at %s", path) + logrus.Debugf("hydrating map value at %s", path) keyEntry := NewEntry(key, yaml.ScalarNode) keyEntry.Value = key container.Content = append(container.Content, keyEntry, newEntry) @@ -303,25 +312,21 @@ func (e *Entry) FromOP(fields []*op.ItemField) error { subContainer := container.ChildNamed(key) if subContainer != nil { container = subContainer - } else { - kind := yaml.MappingNode - if idx+1 < len(path)-1 && isNumeric(path[idx+1]) { - // logrus.Debugf("creating sequence container for key %s at %s", key, path) - kind = yaml.SequenceNode - } - child := NewEntry(key, kind) - child.Path = append(container.Path, key) // nolint: gocritic - - if kind == yaml.SequenceNode { - container.Content = append(container.Content, child) - } else { - // logrus.Debugf("creating mapping container for %s at %s", key, container.Path) - keyEntry := NewEntry(child.Name(), yaml.ScalarNode) - keyEntry.Value = key - container.Content = append(container.Content, keyEntry, child) - } - container = child + continue } + + kind := yaml.MappingNode + if idx+1 == len(path)-1 && isNumeric(path[idx+1]) { + logrus.Debugf("creating sequence container for key %s at %s", key, path) + kind = yaml.SequenceNode + } + child := NewEntry(key, kind) + child.Path = append(container.Path, key) // nolint: gocritic + + keyEntry := NewEntry(child.Name(), yaml.ScalarNode) + keyEntry.Value = key + container.Content = append(container.Content, keyEntry, child) + container = child } } @@ -332,7 +337,7 @@ func (e *Entry) ToOP() []*op.ItemField { ret := []*op.ItemField{} var section *op.ItemSection - if e.Kind == yaml.ScalarNode { + if e.IsScalar() { name := e.Path[len(e.Path)-1] fullPath := strings.Join(e.Path, ".") if len(e.Path) > 1 { @@ -466,10 +471,8 @@ func (e *Entry) Merge(other *Entry) error { if err := local.Merge(remote); err != nil { return err } - } else { - logrus.Debugf("adding new collection value at %s", remote.Path) - local.Content = append(local.Content, remote) } + local.Content = append(local.Content, remote) } return nil diff --git a/pkg/config/input.go b/pkg/config/input.go index 8ee733c..2dfd11b 100644 --- a/pkg/config/input.go +++ b/pkg/config/input.go @@ -48,7 +48,11 @@ func Load(ref string, preferRemote bool) (*Config, error) { return nil, fmt.Errorf("could not load %s from local as it's not a path", ref) } - return FromFile(ref) + cfg, err := FromFile(ref) + if err != nil { + return nil, fmt.Errorf("could not load file %s: %w", ref, err) + } + return cfg, nil } // FromFile reads a path and returns a config. @@ -86,7 +90,7 @@ func FromYAML(data []byte) (*Config, error) { err := yaml.Unmarshal(data, &cfg.Tree) if err != nil { - return nil, err + return nil, fmt.Errorf("could not parse %w", err) } return cfg, nil diff --git a/pkg/config/lookup.go b/pkg/config/lookup.go index 7d61025..3a1ea77 100644 --- a/pkg/config/lookup.go +++ b/pkg/config/lookup.go @@ -15,22 +15,6 @@ import ( "gopkg.in/yaml.v3" ) -func (cfg *Config) Lookup(query []string) (*Entry, error) { - if len(query) == 0 || len(query) == 1 && query[0] == "." { - return cfg.Tree, nil - } - - entry := cfg.Tree - for _, part := range query { - entry = entry.ChildNamed(part) - if entry == nil { - return nil, fmt.Errorf("value not found at %s of %s", part, query) - } - } - - return entry, nil -} - func findRepoConfig(from string) (*opDetails, error) { parts := strings.Split(from, "/") for i := len(parts); i > 0; i-- { @@ -107,7 +91,7 @@ func AutocompleteKeys(cmd *command.Command, currentValue, config string) ([]stri keys, err := KeysFromYAML(buf) if err != nil { - return nil, flag, err + return nil, flag, fmt.Errorf("could not parse file %s as %w", file, err) } sort.Strings(keys) diff --git a/pkg/config/output.go b/pkg/config/output.go index ed74cbb..f070d58 100644 --- a/pkg/config/output.go +++ b/pkg/config/output.go @@ -84,7 +84,7 @@ func (cfg *Config) ToOP() *op.Item { continue } - if value.Kind == yaml.MappingNode { + if value.Kind == yaml.MappingNode || value.Kind == yaml.SequenceNode { sections = append(sections, &op.ItemSection{ ID: value.Name(), Label: value.Name(), diff --git a/test.yaml b/test.yaml index 25cd8a8..8e964d8 100644 --- a/test.yaml +++ b/test.yaml @@ -4,7 +4,7 @@ _config: !!joao # not sorted on purpose int: 1 # line # foot -string: "pato" +string: pato bool: false secret: !!secret very secret nested: @@ -12,6 +12,10 @@ nested: int: 1 secret: !!secret very secret bool: true + list: + - 1 + - 2 + - 3 list: - one - two