Skip to content

Commit ba50564

Browse files
committed
feat: add dry-run and prune flag for apply command
1 parent cb23c7f commit ba50564

File tree

2 files changed

+256
-50
lines changed

2 files changed

+256
-50
lines changed

cmd/internal/apply/command.go

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ import (
2727

2828
"github.com/goccy/go-yaml"
2929
"github.com/googleapis/genai-toolbox/cmd/internal"
30-
"github.com/googleapis/genai-toolbox/internal/log"
3130
"github.com/spf13/cobra"
3231
)
3332

3433
type applyCmd struct {
3534
*cobra.Command
3635
port int
3736
address string
37+
dryRun bool
38+
prune bool
3839
}
3940

4041
func NewCommand(opts *internal.ToolboxOptions) *cobra.Command {
@@ -48,6 +49,8 @@ func NewCommand(opts *internal.ToolboxOptions) *cobra.Command {
4849
internal.ConfigFileFlags(flags, opts)
4950
flags.StringVarP(&cmd.address, "address", "a", "127.0.0.1", "Address of the server that is running on.")
5051
flags.IntVarP(&cmd.port, "port", "p", 5000, "Port of the server that is listening on.")
52+
flags.BoolVar(&cmd.dryRun, "dry-run", false, "Simulate the apply process and report intended updates.")
53+
flags.BoolVar(&cmd.prune, "prune", false, "Remove resources from the server that are not defined in the provided YAML files.")
5154
cmd.RunE = func(*cobra.Command, []string) error { return runApply(cmd, opts) }
5255
return cmd.Command
5356
}
@@ -92,6 +95,16 @@ func (p primitives) Remove(kind, name string) {
9295
}
9396
}
9497

98+
func (p primitives) GetRemaining() []struct{ kind, name string } {
99+
var remaining []struct{ kind, name string }
100+
for kind, names := range p {
101+
for name := range names {
102+
remaining = append(remaining, struct{ kind, name string }{kind, name})
103+
}
104+
}
105+
return remaining
106+
}
107+
95108
// adminRequest is a generic helper for admin api requests.
96109
func adminRequest(ctx context.Context, method, url string, body any) ([]byte, error) {
97110
var bodyReader io.Reader
@@ -171,6 +184,17 @@ func applyPrimitive(ctx context.Context, address string, port int, kind, name st
171184
return nil
172185
}
173186

187+
func deletePrimitive(ctx context.Context, address string, port int, kind, name string) error {
188+
url := fmt.Sprintf("http://%s:%d/admin/%s/%s", address, port, kind, name)
189+
190+
_, err := adminRequest(ctx, http.MethodDelete, url, nil)
191+
if err != nil {
192+
return err
193+
}
194+
195+
return nil
196+
}
197+
174198
func runApply(cmd *applyCmd, opts *internal.ToolboxOptions) error {
175199
ctx, cancel := context.WithCancel(cmd.Context())
176200
defer cancel()
@@ -196,9 +220,11 @@ func runApply(cmd *applyCmd, opts *internal.ToolboxOptions) error {
196220
return err
197221
}
198222

199-
opts.Logger.InfoContext(ctx, "starting apply sequence", "count", len(filePaths))
223+
if cmd.dryRun {
224+
opts.Logger.InfoContext(ctx, "simulating dry run")
225+
}
200226
for _, filePath := range filePaths {
201-
if err := processFile(ctx, opts.Logger, filePath, p, cmd.address, cmd.port); err != nil {
227+
if err := processFile(ctx, opts, filePath, p, cmd); err != nil {
202228
opts.Logger.ErrorContext(ctx, err.Error())
203229
return err
204230
}
@@ -207,14 +233,15 @@ func runApply(cmd *applyCmd, opts *internal.ToolboxOptions) error {
207233
return nil
208234
}
209235

210-
func processFile(ctx context.Context, logger log.Logger, path string, p primitives, address string, port int) error {
236+
func processFile(ctx context.Context, opts *internal.ToolboxOptions, path string, p primitives, cmd *applyCmd) error {
211237
f, err := os.Open(path)
212238
if err != nil {
213239
return fmt.Errorf("unable to open file at %q: %w", path, err)
214240
}
215241
defer f.Close() // Safe closure
216242

217243
decoder := yaml.NewDecoder(f)
244+
opts.Logger.InfoContext(ctx, "starting apply sequence", "count", len(path))
218245
// loop through documents with the `---` separator
219246
for {
220247
var doc map[string]any
@@ -231,15 +258,15 @@ func processFile(ctx context.Context, logger log.Logger, path string, p primitiv
231258
kind, kOk := doc["kind"].(string)
232259
name, nOk := doc["name"].(string)
233260
if !kOk || !nOk || kind == "" || name == "" {
234-
logger.WarnContext(ctx, fmt.Sprintf("invalid primitive schema: missing metadata in %s: kind and name are required", path))
261+
opts.Logger.WarnContext(ctx, fmt.Sprintf("invalid primitive schema: missing metadata in %s: kind and name are required", path))
235262
continue
236263
}
237264

238265
delete(doc, "kind")
239266

240267
if p.Exists(kind, name) {
241268
p.Remove(kind, name)
242-
remoteBody, err := getPrimitiveByName(ctx, address, port, kind, name)
269+
remoteBody, err := getPrimitiveByName(ctx, cmd.address, cmd.port, kind, name)
243270
if err != nil {
244271
return err
245272
}
@@ -253,19 +280,41 @@ func processFile(ctx context.Context, logger log.Logger, path string, p primitiv
253280
}
254281

255282
if bytes.Equal(localJSON, remoteJSON) {
256-
logger.DebugContext(ctx, "skipping: no changes detected", "kind", kind, "name", name)
283+
opts.Logger.DebugContext(ctx, "skipping: no changes detected", "kind", kind, "name", name)
257284
continue
258285
}
259-
logger.DebugContext(ctx, "change detected, updating resource", "kind", kind, "name", name)
286+
opts.Logger.DebugContext(ctx, "change detected, updating resource", "kind", kind, "name", name)
260287
}
261288

262289
// TODO: check --prune flag: if prune, delete primitives that are left
263290
// in the primitive list
264-
// TODO: check for --dry-run flag.
265291

266-
if err := applyPrimitive(ctx, address, port, kind, name, doc); err != nil {
292+
if cmd.dryRun {
293+
fmt.Fprintf(opts.IOStreams.Out, "[dry-run] would update %s: %s\n", kind, name)
294+
continue
295+
}
296+
if err := applyPrimitive(ctx, cmd.address, cmd.port, kind, name, doc); err != nil {
267297
return err
268298
}
269299
}
300+
if cmd.prune {
301+
remaining := p.GetRemaining()
302+
opts.Logger.InfoContext(ctx, "starting prune sequence", "count", len(remaining))
303+
for _, resource := range remaining {
304+
if cmd.dryRun {
305+
fmt.Fprintf(opts.IOStreams.Out, "[dry-run] would delete %s: %s\n", resource.kind, resource.name)
306+
continue
307+
}
308+
309+
// Perform the actual deletion
310+
if err := deletePrimitive(ctx, cmd.address, cmd.port, resource.kind, resource.name); err != nil {
311+
return fmt.Errorf("failed to prune %s/%s: %w", resource.kind, resource.name, err)
312+
}
313+
314+
// Remove from map so we don't try to delete it again
315+
// if processFile is called for multiple files
316+
p.Remove(resource.kind, resource.name)
317+
}
318+
}
270319
return nil
271320
}

0 commit comments

Comments
 (0)