output diff on fetch/flush --dry-run

This commit is contained in:
Roberto Hidalgo 2023-01-17 16:25:18 -06:00
parent 3d84f46112
commit e3297de534
9 changed files with 101 additions and 67 deletions

View File

@ -11,7 +11,7 @@ import (
var Diff = &command.Command{ var Diff = &command.Command{
Path: []string{"diff"}, Path: []string{"diff"},
Summary: "Shows differences between local and remote configs", Summary: "Shows differences between local and remote configs",
Description: `Fetches remote and compares against local, ignoring comments but respecting order.`, Description: `Fetches remote and compares against local, ignoring comments but respecting order. The diff output shows what would happen upon running ﹅joao fetch﹅. Specify ﹅--remote﹅ to show what would happen upon ﹅joao flush﹅`,
Arguments: command.Arguments{ Arguments: command.Arguments{
{ {
Name: "config", Name: "config",
@ -26,7 +26,7 @@ var Diff = &command.Command{
Options: command.Options{ Options: command.Options{
"output": { "output": {
Description: "How to format the differences", Description: "How to format the differences",
Type: "string", Type: command.ValueTypeString,
Default: "auto", Default: "auto",
Values: &command.ValueSource{ Values: &command.ValueSource{
Static: &[]string{ Static: &[]string{
@ -34,15 +34,21 @@ var Diff = &command.Command{
}, },
}, },
}, },
"remote": {
Description: "Shows what would happen on `flush` instead of `fetch`",
Type: command.ValueTypeString,
Default: false,
},
"redacted": { "redacted": {
Description: "Compare redacted versions", Description: "Compare redacted versions",
Type: "bool", Type: command.ValueTypeBoolean,
Default: false, Default: false,
}, },
}, },
Action: func(cmd *command.Command) error { Action: func(cmd *command.Command) error {
paths := cmd.Arguments[0].ToValue().([]string) paths := cmd.Arguments[0].ToValue().([]string)
redacted := cmd.Options["redacted"].ToValue().(bool) redacted := cmd.Options["redacted"].ToValue().(bool)
remote := cmd.Options["remote"].ToValue().(bool)
for _, path := range paths { for _, path := range paths {
local, err := config.Load(path, false) local, err := config.Load(path, false)
@ -50,7 +56,7 @@ var Diff = &command.Command{
return err return err
} }
if err := local.DiffRemote(path, redacted, cmd.Cobra.OutOrStdout(), cmd.Cobra.OutOrStderr()); err != nil { if err := local.DiffRemote(path, redacted, remote, cmd.Cobra.OutOrStdout(), cmd.Cobra.OutOrStderr()); err != nil {
return err return err
} }
} }

View File

@ -32,11 +32,21 @@ var Fetch = &command.Command{
Action: func(cmd *command.Command) error { Action: func(cmd *command.Command) error {
paths := cmd.Arguments[0].ToValue().([]string) paths := cmd.Arguments[0].ToValue().([]string)
for _, path := range paths { for _, path := range paths {
remote, err := config.Load(path, true) local, err := config.Load(path, false)
if err != nil { if err != nil {
return err return err
} }
local, err := config.Load(path, false)
if dryRun := cmd.Options["dry-run"].ToValue().(bool); dryRun {
logrus.Warnf("dry-run: comparing %s to %s", local.OPURL(), path)
if err := local.DiffRemote(path, false, true, cmd.Cobra.OutOrStdout(), cmd.Cobra.OutOrStderr()); err != nil {
return err
}
logrus.Warnf("dry-run: did not update %s", path)
continue
}
remote, err := config.Load(path, true)
if err != nil { if err != nil {
return err return err
} }
@ -45,20 +55,11 @@ var Fetch = &command.Command{
return err return err
} }
if dryRun := cmd.Options["dry-run"].ToValue().(bool); dryRun {
b, err := local.AsYAML()
if err != nil {
return err
}
logrus.Warnf("dry-run: would write to %s", path)
_, _ = cmd.Cobra.OutOrStdout().Write(b)
} else {
if err := local.AsFile(path); err != nil { if err := local.AsFile(path); err != nil {
return err return err
} }
}
logrus.Infof("Updated %s", path) logrus.Infof("Fetched %s => %s", remote.OPURL(), path)
} }
logrus.Info("Done") logrus.Info("Done")

View File

@ -69,36 +69,26 @@ func TestFetch(t *testing.T) {
t.Fatalf("could not get: %s", err) t.Fatalf("could not get: %s", err)
} }
expected := `_config: !!joao expected := `--- /Users/roberto/src/joao/test.yaml
name: some:test +++ op://example/some:test
vault: example @@ -1,4 +1,8 @@
# not sorted on purpose
int: 1 # line
# foot
string: pato
bool: false bool: false
secret: !!secret very secret +e-fez-tambem:
nested: + - quém!
+ - quém!
+ - quém!
int: 1 int: 1
bool: true
list:
- 1
- 2
- 3
secret: !!secret very secret
second_secret: !!secret very secret
string: quem
list: list:
- one - one
- two @@ -14,5 +18,8 @@
- three second_secret: !!secret very secret
o: secret: !!secret very secret
ganso: string: quem
gosto: da dupla +o:
e-fez-tambem: + ganso:
- quém! + gosto: da dupla
- quém! secret: !!secret very secret
- quém!` string: pato`
if got := out.String(); 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) t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)

View File

@ -38,8 +38,9 @@ var Flush = &command.Command{
}, },
Action: func(cmd *command.Command) error { Action: func(cmd *command.Command) error {
paths := cmd.Arguments[0].ToValue().([]string) paths := cmd.Arguments[0].ToValue().([]string)
dryRun := cmd.Options["dry-run"].ToValue().(bool)
if dryRun := cmd.Options["dry-run"].ToValue().(bool); dryRun { if dryRun {
opclient.Use(&opclient.CLI{DryRun: true}) opclient.Use(&opclient.CLI{DryRun: true})
} }
@ -49,6 +50,15 @@ var Flush = &command.Command{
return err return err
} }
if dryRun {
logrus.Warnf("dry-run: comparing %s to %s", path, cfg.OPURL())
if err := cfg.DiffRemote(path, false, false, cmd.Cobra.OutOrStdout(), cmd.Cobra.OutOrStderr()); err != nil {
return err
}
logrus.Warnf("dry-run: did not update %s", cfg.OPURL())
continue
}
if err := opclient.Update(cfg.Vault, cfg.Name, cfg.ToOP()); err != nil { if err := opclient.Update(cfg.Vault, cfg.Name, cfg.ToOP()); err != nil {
return fmt.Errorf("could not flush to 1password: %w", err) return fmt.Errorf("could not flush to 1password: %w", err)
} }
@ -58,6 +68,7 @@ var Flush = &command.Command{
return err return err
} }
} }
logrus.Infof("Flushed %s to %s", path, cfg.OPURL())
} }
logrus.Info("Done") logrus.Info("Done")

View File

@ -34,7 +34,7 @@ func Get(vault, name string) (*op.Item, error) {
func Update(vault, name string, item *op.Item) error { func Update(vault, name string, item *op.Item) error {
remote, err := client.Get(vault, name) remote, err := client.Get(vault, name)
if err != nil { if err != nil {
if strings.Contains(err.Error(), fmt.Sprintf("\"%s\" isn't an item in the ", name)) { if strings.Contains(err.Error(), fmt.Sprintf("\"%s\" isn't an item in ", name)) {
return client.Create(item) return client.Create(item)
} }

View File

@ -156,18 +156,29 @@ func (cfg *Config) Merge(other *Config) error {
return cfg.Tree.Merge(other.Tree) return cfg.Tree.Merge(other.Tree)
} }
func (cfg *Config) DiffRemote(path string, redacted bool, stdout, stderr io.Writer) error { func (cfg *Config) OPURL() string {
return fmt.Sprintf("op://%s/%s", cfg.Vault, cfg.Name)
}
func (cfg *Config) DiffRemote(path string, redacted, asFetch bool, stdout, stderr io.Writer) error {
logrus.Debugf("loading remote for %s", path) logrus.Debugf("loading remote for %s", path)
remote, err := Load(path, true) remote, err := Load(path, true)
if err != nil { if err != nil {
if asFetch {
return err return err
} }
if !strings.Contains(err.Error(), " isn't an item in ") {
return fmt.Errorf("could not fetch remote item: %w", err)
}
}
modes := []OutputMode{OutputModeNoComments, OutputModeSorted, OutputModeNoConfig, OutputModeStandardYAML} modes := []OutputMode{OutputModeNoComments, OutputModeSorted, OutputModeNoConfig, OutputModeStandardYAML}
if redacted { if redacted {
modes = append(modes, OutputModeRedacted) modes = append(modes, OutputModeRedacted)
} }
logrus.Debugf("loading local for %s", path)
localBytes, err := cfg.AsYAML(modes...) localBytes, err := cfg.AsYAML(modes...)
if err != nil { if err != nil {
return err return err
@ -179,18 +190,30 @@ func (cfg *Config) DiffRemote(path string, redacted bool, stdout, stderr io.Writ
} }
defer cleanupLocalDiff() defer cleanupLocalDiff()
file2 := "/dev/null"
opPath := "(new) " + cfg.OPURL()
if remote != nil {
remoteBytes, err := remote.AsYAML(modes...) remoteBytes, err := remote.AsYAML(modes...)
if err != nil { if err != nil {
return err return err
} }
file2, cleanupRemoteDiff, err := tempfile(remoteBytes) f2, cleanupRemoteDiff, err := tempfile(remoteBytes)
if err != nil { if err != nil {
return err return err
} }
file2 = f2
opPath = remote.OPURL()
defer cleanupRemoteDiff() defer cleanupRemoteDiff()
}
var diff *exec.Cmd
if asFetch {
diff = exec.Command("diff", "-u", "-L", path, file1, "-L", opPath, file2)
} else {
diff = exec.Command("diff", "-u", "-L", opPath, file2, "-L", path, file1)
}
opPath := fmt.Sprintf("op://%s/%s", cfg.Vault, remote.Name)
diff := exec.Command("diff", "-u", "-L", path, file1, "-L", opPath, file2)
diff.Env = os.Environ() diff.Env = os.Environ()
diff.Stdout = stdout diff.Stdout = stdout

View File

@ -197,7 +197,7 @@ func (e *Entry) asNode() *yaml.Node {
if yamlOutput.Has(OutputModeStandardYAML) { if yamlOutput.Has(OutputModeStandardYAML) {
if e.IsScalar() { if e.IsScalar() {
if len(e.Path) >= 0 { if len(e.Path) > 0 {
if !strings.Contains(n.Value, "\n") { if !strings.Contains(n.Value, "\n") {
n.Style &= yaml.FoldedStyle n.Style &= yaml.FoldedStyle
} else { } else {

View File

@ -24,8 +24,11 @@ func Load(ref string, preferRemote bool) (*Config, error) {
vault := "" vault := ""
if argIsYAMLFile(ref) { if argIsYAMLFile(ref) {
var err error path, err := filepath.Abs(ref)
name, vault, err = VaultAndNameFrom(ref, nil) if err != nil {
return nil, fmt.Errorf("could not find asbolute path to file %s: %w", ref, err)
}
name, vault, err = VaultAndNameFrom(path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -43,7 +43,7 @@ const (
OutputModeSorted OutputMode = 4 OutputModeSorted OutputMode = 4
// OutputModeNoConfig does not output the _config key if any. // OutputModeNoConfig does not output the _config key if any.
OutputModeNoConfig OutputMode = 8 OutputModeNoConfig OutputMode = 8
// OutputModeStandardYAML formats strings and arrays uniformly // OutputModeStandardYAML formats strings and arrays uniformly.
OutputModeStandardYAML OutputMode = 16 OutputModeStandardYAML OutputMode = 16
) )