test about half of what matters

This commit is contained in:
Roberto Hidalgo 2023-01-10 22:50:06 -06:00
parent 6f163b5e22
commit 632af1a2be
16 changed files with 989 additions and 107 deletions

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2021 Roberto Hidalgo <joao@un.rob.mx>
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"

View File

@ -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

5
bad-test.yaml Normal file
View File

@ -0,0 +1,5 @@
_config: !!joao
name: some:test
vault: bad-example
int: -:a\

105
cmd/fetch_test.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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)
}
}

View File

@ -77,6 +77,7 @@ looks at the filesystem or remotely, using 1password (over the CLI if available,
} }
if query == "" || query == "." { if query == "" || query == "." {
var bytes []byte
switch format { switch format {
case "yaml", "raw", "diff-yaml": case "yaml", "raw", "diff-yaml":
modes := []config.OutputMode{} modes := []config.OutputMode{}
@ -86,21 +87,17 @@ looks at the filesystem or remotely, using 1password (over the CLI if available,
if format == "diff-yaml" { if format == "diff-yaml" {
modes = append(modes, config.OutputModeNoComments, config.OutputModeSorted) modes = append(modes, config.OutputModeNoComments, config.OutputModeSorted)
} }
bytes, err := cfg.AsYAML(modes...) bytes, err = cfg.AsYAML(modes...)
if err != nil {
return err
}
_, err = cmd.Cobra.OutOrStdout().Write(bytes)
return err
case "json", "op": case "json", "op":
bytes, err := cfg.AsJSON(redacted, format == "op") bytes, err = cfg.AsJSON(redacted, format == "op")
if err != nil { default:
return err return fmt.Errorf("unknown format %s", format)
} }
_, err = cmd.Cobra.OutOrStdout().Write(bytes) if err != nil {
return err return err
} }
return fmt.Errorf("unknown format %s", format) _, err = cmd.Cobra.OutOrStdout().Write(bytes)
return err
} }
parts := strings.Split(query, ".") parts := strings.Split(query, ".")

View File

@ -11,9 +11,166 @@ import (
"testing" "testing"
. "git.rob.mx/nidito/joao/cmd" . "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" "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 { func fromProjectRoot() string {
_, filename, _, _ := runtime.Caller(0) _, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "../") dir := path.Join(path.Dir(filename), "../")
@ -24,29 +181,42 @@ func fromProjectRoot() string {
return wd return wd
} }
func TestGetRedacted(t *testing.T) { func TestGetBadYAML(t *testing.T) {
root := fromProjectRoot() root := fromProjectRoot()
out := bytes.Buffer{}
Get.SetBindings() Get.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{} cmd := &cobra.Command{}
cmd.Flags().Bool("redacted", true, "")
cmd.SetOut(&out) cmd.SetOut(&out)
cmd.SetErr(&out) cmd.SetErr(&out)
Get.Cobra = cmd Get.Cobra = cmd
err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--redacted"}) err := Get.Run(cmd, []string{root + "/bad-test.yaml", "."})
if err == nil {
if err != nil { t.Fatalf("Did not throw on bad path: %s", out.String())
t.Fatalf("could not get: %s", err)
} }
wantedPrefix := "could not parse file"
expected, err := os.ReadFile(root + "/test.yaml") wantedSuffix := "/bad-test.yaml as yaml: line 4: mapping values are not allowed in this context"
if err != nil { if got := err.Error(); !(strings.HasPrefix(got, wantedPrefix) && strings.HasSuffix(got, wantedSuffix)) {
t.Fatalf("could not read file: %s", err) t.Fatalf("Failed with bad error, wanted %s /some-path%s, got %s", wantedPrefix, wantedSuffix, got)
} }
}
got := out.String() func TestGetBadPath(t *testing.T) {
if strings.TrimSpace(got) != strings.ReplaceAll(strings.TrimSpace(string(expected)), " very secret", "") { root := fromProjectRoot()
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) 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) t.Fatalf("could not read file: %s", err)
} }
got := out.String() if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(string(expected)) {
if 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) t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
} }
} }
@ -82,6 +276,7 @@ func TestGetPath(t *testing.T) {
Get.SetBindings() Get.SetBindings()
cmd := &cobra.Command{} cmd := &cobra.Command{}
cmd.Flags().Bool("redacted", false, "") cmd.Flags().Bool("redacted", false, "")
cmd.Flags().StringP("output", "o", "yaml", "")
cmd.SetOut(&out) cmd.SetOut(&out)
cmd.SetErr(&out) cmd.SetErr(&out)
Get.Cobra = cmd Get.Cobra = cmd
@ -92,8 +287,29 @@ func TestGetPath(t *testing.T) {
} }
expected := "very secret" expected := "very secret"
got := out.String() if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) {
if 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) 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 expected := `bool: true
int: 1 int: 1
list:
- 1
- 2
- 3
secret: very secret secret: very secret
string: quem` string: quem`
got := out.String() if got := out.String(); strings.TrimSpace(got) != expected {
if strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
} }
} }
@ -153,13 +372,16 @@ list:
nested: nested:
bool: true bool: true
int: 1 int: 1
list:
- 1
- 2
- 3
secret: !!secret very secret secret: !!secret very secret
string: quem string: quem
secret: !!secret very secret secret: !!secret very secret
string: "pato"` string: pato`
got := out.String() if got := out.String(); strings.TrimSpace(got) != expected {
if strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) 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) 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 got := out.String(); strings.TrimSpace(got) != expected {
if strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) 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) t.Fatalf("could not get: %s", err)
} }
expected := `very secret` expected := `very secret` // nolint: ifshort
got := out.String() if got := out.String(); strings.TrimSpace(got) != expected {
if strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) 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) 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 got := out.String(); strings.TrimSpace(got) != expected {
if strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) 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) 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 got := out.String(); strings.TrimSpace(got) != expected {
if strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got) 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) 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 got := out.String(); strings.TrimSpace(got) != expected {
if 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) t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
} }
} }

View File

@ -5,6 +5,7 @@ package cmd
import ( import (
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil"
"os" "os"
"strings" "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" { if delete && input != "/dev/stdin" {
logrus.Warn("Ignoring --file while deleting") logrus.Warn("Ignoring --input while deleting")
} }
cfg, err = config.Load(path, false) 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 return err
} }
} else { } 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 { if err != nil {
return err return err
} }

457
cmd/set_test.go Normal file
View File

@ -0,0 +1,457 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// 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)
}
}

View File

@ -12,7 +12,7 @@ import (
var Plugin = &command.Command{ var Plugin = &command.Command{
Path: []string{"vault", "server"}, Path: []string{"vault", "server"},
Summary: "Starts a vault-joao-plugin 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. 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.

View File

@ -3,10 +3,14 @@
package opclient package opclient
import ( import (
"strings"
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
op "github.com/1Password/connect-sdk-go/onepassword" 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. // UUIDLength defines the required length of UUIDs.
const UUIDLength = 26 const UUIDLength = 26
@ -33,7 +37,7 @@ type Connect struct {
const userAgent = "nidito-joao" const userAgent = "nidito-joao"
func NewConnect(host, token string) *Connect { func NewConnect(host, token string) *Connect {
client := connect.NewClientWithUserAgent(host, token, userAgent) client := ConnectClientFactory(host, token, userAgent)
return &Connect{client: client} return &Connect{client: client}
} }
@ -41,12 +45,27 @@ func (b *Connect) Get(vault, name string) (*op.Item, error) {
return b.client.GetItem(name, vault) 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) _, err := b.client.UpdateItem(item, item.Vault.ID)
return err return err
} }
func (b *Connect) List(vault, prefix string) ([]string, error) { func (b *Connect) List(vault, prefix string) ([]string, error) {
// TODO: get this done items, err := b.client.GetItems(vault)
return nil, nil 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
} }

View File

@ -89,6 +89,12 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er
if err := yaml.Unmarshal(data, newEntry); err != nil { if err := yaml.Unmarshal(data, newEntry); err != nil {
return err 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 { } else {
valueStr = strings.Trim(valueStr, "\n") valueStr = strings.Trim(valueStr, "\n")
if isSecret { if isSecret {
@ -108,12 +114,14 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er
entry := cfg.Tree entry := cfg.Tree
for idx, key := range path { for idx, key := range path {
if len(path)-1 == idx { if len(path)-1 == idx {
dst := entry.ChildNamed(key) if dst := entry.ChildNamed(key); dst == nil {
if dst == nil { key := NewEntry(key, yaml.ScalarNode)
if entry.Kind == yaml.MappingNode { 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) entry.Content = append(entry.Content, key, newEntry)
} else { } else {
logrus.Infof("setting new list key %v", newEntry.Path)
entry.Kind = yaml.SequenceNode
entry.Content = append(entry.Content, newEntry) entry.Content = append(entry.Content, newEntry)
} }
} else { } else {
@ -131,12 +139,15 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er
} }
kind := yaml.MappingNode kind := yaml.MappingNode
if isNumeric(key) { if idx+1 == len(path)-1 && isNumeric(path[idx+1]) {
kind = yaml.SequenceNode kind = yaml.SequenceNode
} }
sub := NewEntry(key, kind) sub := NewEntry(key, kind)
sub.Path = append(entry.Path, key) // nolint: gocritic 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 entry = sub
} }

View File

@ -206,6 +206,10 @@ func (e *Entry) MarshalYAML() (*yaml.Node, error) {
} }
} else { } else {
entries := e.Contents() 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 { for i := 0; i < len(entries); i += 2 {
key := entries[i] key := entries[i]
value := entries[i+1] value := entries[i+1]
@ -213,6 +217,11 @@ func (e *Entry) MarshalYAML() (*yaml.Node, error) {
continue continue
} }
if key.Type == "" {
key.Kind = yaml.ScalarNode
key.Type = "!!map"
}
keyNode, err := key.MarshalYAML() keyNode, err := key.MarshalYAML()
if err != nil { if err != nil {
return nil, err return nil, err
@ -288,11 +297,11 @@ func (e *Entry) FromOP(fields []*op.ItemField) error {
Type: kind, Type: kind,
} }
if isNumeric(key) { if isNumeric(key) {
// logrus.Debugf("hydrating sequence value at %s", path) logrus.Debugf("hydrating sequence value at %s", path)
container.Kind = yaml.SequenceNode container.Kind = yaml.SequenceNode
container.Content = append(container.Content, newEntry) container.Content = append(container.Content, newEntry)
} else { } else {
// logrus.Debugf("hydrating map value at %s", path) logrus.Debugf("hydrating map value at %s", path)
keyEntry := NewEntry(key, yaml.ScalarNode) keyEntry := NewEntry(key, yaml.ScalarNode)
keyEntry.Value = key keyEntry.Value = key
container.Content = append(container.Content, keyEntry, newEntry) container.Content = append(container.Content, keyEntry, newEntry)
@ -303,25 +312,21 @@ func (e *Entry) FromOP(fields []*op.ItemField) error {
subContainer := container.ChildNamed(key) subContainer := container.ChildNamed(key)
if subContainer != nil { if subContainer != nil {
container = subContainer container = subContainer
} else { 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
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
} }
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{} ret := []*op.ItemField{}
var section *op.ItemSection var section *op.ItemSection
if e.Kind == yaml.ScalarNode { if e.IsScalar() {
name := e.Path[len(e.Path)-1] name := e.Path[len(e.Path)-1]
fullPath := strings.Join(e.Path, ".") fullPath := strings.Join(e.Path, ".")
if len(e.Path) > 1 { if len(e.Path) > 1 {
@ -466,10 +471,8 @@ func (e *Entry) Merge(other *Entry) error {
if err := local.Merge(remote); err != nil { if err := local.Merge(remote); err != nil {
return err 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 return nil

View File

@ -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 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. // FromFile reads a path and returns a config.
@ -86,7 +90,7 @@ func FromYAML(data []byte) (*Config, error) {
err := yaml.Unmarshal(data, &cfg.Tree) err := yaml.Unmarshal(data, &cfg.Tree)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("could not parse %w", err)
} }
return cfg, nil return cfg, nil

View File

@ -15,22 +15,6 @@ import (
"gopkg.in/yaml.v3" "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) { func findRepoConfig(from string) (*opDetails, error) {
parts := strings.Split(from, "/") parts := strings.Split(from, "/")
for i := len(parts); i > 0; i-- { for i := len(parts); i > 0; i-- {
@ -107,7 +91,7 @@ func AutocompleteKeys(cmd *command.Command, currentValue, config string) ([]stri
keys, err := KeysFromYAML(buf) keys, err := KeysFromYAML(buf)
if err != nil { if err != nil {
return nil, flag, err return nil, flag, fmt.Errorf("could not parse file %s as %w", file, err)
} }
sort.Strings(keys) sort.Strings(keys)

View File

@ -84,7 +84,7 @@ func (cfg *Config) ToOP() *op.Item {
continue continue
} }
if value.Kind == yaml.MappingNode { if value.Kind == yaml.MappingNode || value.Kind == yaml.SequenceNode {
sections = append(sections, &op.ItemSection{ sections = append(sections, &op.ItemSection{
ID: value.Name(), ID: value.Name(),
Label: value.Name(), Label: value.Name(),

View File

@ -4,7 +4,7 @@ _config: !!joao
# not sorted on purpose # not sorted on purpose
int: 1 # line int: 1 # line
# foot # foot
string: "pato" string: pato
bool: false bool: false
secret: !!secret very secret secret: !!secret very secret
nested: nested:
@ -12,6 +12,10 @@ nested:
int: 1 int: 1
secret: !!secret very secret secret: !!secret very secret
bool: true bool: true
list:
- 1
- 2
- 3
list: list:
- one - one
- two - two