Skip to content

Commit e225952

Browse files
committed
Add update from json and y/n
Signed-off-by: Adolfo García Veytia (Puerco) <puerco@carabiner.dev>
1 parent a473d09 commit e225952

1 file changed

Lines changed: 93 additions & 25 deletions

File tree

internal/cmd/update.go

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
package cmd
55

66
import (
7+
"bufio"
78
"encoding/json"
89
"fmt"
910
"os"
1011
"sort"
12+
"strings"
1113

1214
"github.com/carabiner-dev/policy"
15+
api "github.com/carabiner-dev/policy/api/v1"
1316
"github.com/spf13/cobra"
1417
"google.golang.org/protobuf/encoding/protojson"
15-
16-
api "github.com/carabiner-dev/policy/api/v1"
1718
)
1819

1920
func addUpdate(parentCmd *cobra.Command) {
20-
var fromJSON string
21+
var (
22+
fromJSON string
23+
force bool
24+
)
2125
updateCmd := &cobra.Command{
2226
Short: "update policy references to their latest versions",
2327
Use: "update [flags] <location> [<location>...]",
@@ -28,6 +32,10 @@ policy source files in place.
2832
Only filesystem locations are supported: remote (VCS locator) locations
2933
will be skipped.
3034
35+
By default, the available updates are displayed as a table and the user
36+
is asked to confirm before anything is written. Use --force to skip the
37+
prompt and apply updates directly.
38+
3139
When --from-json is provided, updates are read from the given plan file
3240
instead of being computed from scratch. The plan file is the JSON
3341
document produced by 'policyctl check-update --format=json'; no remote
@@ -42,7 +50,6 @@ the strings that actually changed.`,
4250
PersistentPreRunE: initLogging,
4351
RunE: func(cmd *cobra.Command, args []string) error {
4452
cmd.SilenceUsage = true
45-
4653
u := policy.NewUpdater()
4754

4855
var (
@@ -54,48 +61,109 @@ the strings that actually changed.`,
5461
if len(args) > 0 {
5562
return fmt.Errorf("--from-json cannot be combined with positional locations")
5663
}
57-
updates, lerr := loadPlanFile(fromJSON)
58-
if lerr != nil {
59-
return lerr
60-
}
61-
applied, err = u.ApplyUpdates(updates)
64+
applied, err = runFromJSON(u, fromJSON, force)
6265
default:
6366
if len(args) == 0 {
6467
return fmt.Errorf("at least one location is required (or use --from-json)")
6568
}
66-
applied, err = u.Update(args...)
69+
applied, err = runFromLocations(u, args, force)
6770
}
6871
if err != nil {
6972
return err
7073
}
71-
72-
if len(applied) == 0 {
73-
fmt.Fprintln(os.Stderr, "no policy references needed updating")
74-
return nil
75-
}
76-
77-
files := make([]string, 0, len(applied))
78-
for f := range applied {
79-
files = append(files, f)
80-
}
81-
sort.Strings(files)
82-
for _, f := range files {
83-
fmt.Fprintf(os.Stdout, "updated %s (%d reference(s))\n", f, len(applied[f]))
84-
}
74+
reportApplied(applied)
8575
return nil
8676
},
8777
}
8878
updateCmd.Flags().StringVar(
8979
&fromJSON, "from-json", "",
9080
"apply updates from a plan file produced by 'check-update --format=json'",
9181
)
82+
updateCmd.Flags().BoolVar(
83+
&force, "force", false,
84+
"skip the confirmation prompt and apply updates directly",
85+
)
9286
parentCmd.AddCommand(updateCmd)
9387
}
9488

89+
// runFromLocations handles the default path: check for updates, show
90+
// them, ask for confirmation, then apply. When force is set, skips both
91+
// the table and the prompt and delegates to Updater.Update.
92+
func runFromLocations(u *policy.Updater, args []string, force bool) (map[string][]*policy.RefUpdate, error) {
93+
if force {
94+
return u.Update(args...)
95+
}
96+
97+
updates, err := u.CheckUpdates(args...)
98+
if err != nil {
99+
return nil, err
100+
}
101+
if len(updates) == 0 {
102+
fmt.Fprintln(os.Stderr, "no policy references have updates available")
103+
return nil, nil
104+
}
105+
printUpdatesTable(os.Stdout, updates)
106+
if !confirm("Apply these updates?") {
107+
fmt.Fprintln(os.Stderr, "aborted; no files were modified")
108+
return nil, nil
109+
}
110+
return u.ApplyUpdates(updates)
111+
}
112+
113+
// runFromJSON handles the --from-json path: decode the plan, show it,
114+
// optionally prompt, then apply.
115+
func runFromJSON(u *policy.Updater, path string, force bool) (map[string][]*policy.RefUpdate, error) {
116+
updates, err := loadPlanFile(path)
117+
if err != nil {
118+
return nil, err
119+
}
120+
if len(updates) == 0 {
121+
fmt.Fprintln(os.Stderr, "plan file contains no updates to apply")
122+
return nil, nil
123+
}
124+
if !force {
125+
printUpdatesTable(os.Stdout, updates)
126+
if !confirm("Apply these updates?") {
127+
fmt.Fprintln(os.Stderr, "aborted; no files were modified")
128+
return nil, nil
129+
}
130+
}
131+
return u.ApplyUpdates(updates)
132+
}
133+
134+
func reportApplied(applied map[string][]*policy.RefUpdate) {
135+
if len(applied) == 0 {
136+
fmt.Fprintln(os.Stderr, "no policy references needed updating")
137+
return
138+
}
139+
files := make([]string, 0, len(applied))
140+
for f := range applied {
141+
files = append(files, f)
142+
}
143+
sort.Strings(files)
144+
for _, f := range files {
145+
fmt.Fprintf(os.Stdout, "updated %s (%d reference(s))\n", f, len(applied[f])) //nolint:errcheck // stdout write errors are not actionable here
146+
for _, r := range applied[f] {
147+
fmt.Fprintf(os.Stdout, " - %s\n", r.Old.GetLocation().GetUri()) //nolint:errcheck // stdout write errors are not actionable here
148+
}
149+
}
150+
}
151+
152+
func confirm(prompt string) bool {
153+
fmt.Fprintf(os.Stderr, "%s [y/N]: ", prompt)
154+
r := bufio.NewReader(os.Stdin)
155+
line, err := r.ReadString('\n')
156+
if err != nil {
157+
return false
158+
}
159+
line = strings.TrimSpace(strings.ToLower(line))
160+
return line == "y" || line == "yes"
161+
}
162+
95163
// loadPlanFile decodes the JSON plan produced by check-update into the
96164
// map shape accepted by Updater.ApplyUpdates.
97165
func loadPlanFile(path string) (map[string][]*policy.RefUpdate, error) {
98-
data, err := os.ReadFile(path) //nolint:gosec // path provided by the user on the command line
166+
data, err := os.ReadFile(path)
99167
if err != nil {
100168
return nil, fmt.Errorf("reading plan file: %w", err)
101169
}

0 commit comments

Comments
 (0)