Skip to content

Commit 12171f7

Browse files
authored
feat: add migrate subcommand (#2679)
Add a `migrate` subcommand to help migrate all configuration files to flat format. This PR also updates the `LoadConfig()` function in `options.go` by extracting the custom config path logic into a new function. This removes duplication and makes the logic reusable for the migrate subcommand.
1 parent cf787a7 commit 12171f7

6 files changed

Lines changed: 638 additions & 70 deletions

File tree

cmd/internal/migrate/command.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package migrate
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"os"
22+
23+
"github.com/google/go-cmp/cmp"
24+
"github.com/googleapis/genai-toolbox/cmd/internal"
25+
"github.com/spf13/cobra"
26+
)
27+
28+
// migrateCmd is the command for migrating configuration files.
29+
type migrateCmd struct {
30+
*cobra.Command
31+
dryRun bool
32+
}
33+
34+
func NewCommand(opts *internal.ToolboxOptions) *cobra.Command {
35+
cmd := &migrateCmd{}
36+
cmd.Command = &cobra.Command{
37+
Use: "migrate",
38+
Short: "Migrate all configuration files to flat format",
39+
Long: "Migrate all configuration files provided to the flat format, updating deprecated fields and ensuring compatibility.",
40+
}
41+
flags := cmd.Flags()
42+
internal.ConfigFileFlags(flags, opts)
43+
flags.BoolVar(&cmd.dryRun, "dry-run", false, "Preview the converted format without applying actual changes.")
44+
cmd.RunE = func(*cobra.Command, []string) error { return runMigrate(cmd, opts) }
45+
return cmd.Command
46+
}
47+
48+
func runMigrate(cmd *migrateCmd, opts *internal.ToolboxOptions) error {
49+
ctx, cancel := context.WithCancel(cmd.Context())
50+
defer cancel()
51+
52+
ctx, shutdown, err := opts.Setup(ctx)
53+
if err != nil {
54+
return err
55+
}
56+
defer func() {
57+
_ = shutdown(ctx)
58+
}()
59+
60+
logger := opts.Logger
61+
filePaths, _, err := opts.GetCustomConfigFiles(ctx)
62+
if err != nil {
63+
errMsg := fmt.Errorf("error retrieving configuration file: %w", err)
64+
logger.ErrorContext(ctx, errMsg.Error())
65+
return errMsg
66+
}
67+
68+
logger.InfoContext(ctx, "migration process will start; any comments present in the original configuration files will not be preserved in the migrated files")
69+
var errs []error
70+
// process each files independently.
71+
for _, filePath := range filePaths {
72+
buf, err := os.ReadFile(filePath)
73+
if err != nil {
74+
errMsg := fmt.Errorf("unable to read tool file at %q: %w", filePath, err)
75+
logger.ErrorContext(ctx, errMsg.Error())
76+
errs = append(errs, errMsg)
77+
continue
78+
}
79+
newBuf, err := internal.ConvertToolsFile(buf)
80+
if err != nil {
81+
logger.ErrorContext(ctx, err.Error())
82+
errs = append(errs, err)
83+
continue
84+
}
85+
if cmp.Equal(buf, newBuf) {
86+
continue
87+
}
88+
89+
if cmd.dryRun {
90+
logger.DebugContext(ctx, fmt.Sprintf("printing migration to output for file: %s", filePath))
91+
fmt.Fprintln(opts.IOStreams.Out, string(newBuf))
92+
} else {
93+
info, err := os.Stat(filePath)
94+
if err != nil {
95+
errMsg := fmt.Errorf("failed to stat file: %w", err)
96+
logger.ErrorContext(ctx, errMsg.Error())
97+
errs = append(errs, errMsg)
98+
continue
99+
}
100+
backupFile := filePath + ".bak"
101+
err = os.Rename(filePath, backupFile)
102+
if err != nil {
103+
errMsg := fmt.Errorf("failed to rename file: %w", err)
104+
logger.ErrorContext(ctx, errMsg.Error())
105+
errs = append(errs, errMsg)
106+
continue
107+
}
108+
logger.DebugContext(ctx, fmt.Sprintf("successfully renamed %s to %s", filePath, backupFile))
109+
110+
// set the permission to the original file's permission.
111+
err = os.WriteFile(filePath, newBuf, info.Mode().Perm())
112+
if err != nil {
113+
errMsg := fmt.Errorf("failed to write to file: %w", err)
114+
// restoring original file
115+
if removeErr := os.Remove(filePath); removeErr != nil { // Attempt to remove the possibly partial file to ensure Rename succeeds.
116+
errMsg = errors.Join(errMsg, removeErr)
117+
}
118+
if restoreErr := os.Rename(backupFile, filePath); restoreErr != nil {
119+
fullRestoreErr := fmt.Errorf("failed to restore original file: %w", restoreErr)
120+
errMsg = errors.Join(errMsg, fullRestoreErr)
121+
}
122+
logger.ErrorContext(ctx, errMsg.Error())
123+
errs = append(errs, errMsg)
124+
continue
125+
}
126+
logger.DebugContext(ctx, fmt.Sprintf("migration completed for file: %s", filePath))
127+
}
128+
}
129+
130+
logger.InfoContext(ctx, "migration completed!")
131+
// If errs is empty, errors.Join returns nil
132+
return errors.Join(errs...)
133+
}

0 commit comments

Comments
 (0)