Skip to content

Commit e66e989

Browse files
authored
Migrate individual servers to default group (#1186)
1 parent 73a3707 commit e66e989

File tree

6 files changed

+161
-32
lines changed

6 files changed

+161
-32
lines changed

cmd/thv/app/run.go

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -127,36 +127,9 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
127127
// Get debug mode flag
128128
debugMode, _ := cmd.Flags().GetBool("debug")
129129

130-
workloadName := runFlags.Name
131-
if workloadName == "" {
132-
workloadName = serverOrImage
133-
}
134-
135-
isForeground := runFlags.Foreground
136-
137-
if runFlags.Group != "" {
138-
groupManager, err := groups.NewManager()
139-
if err != nil {
140-
return fmt.Errorf("failed to create group manager: %v", err)
141-
}
142-
143-
// Check if the workload is already in a group
144-
group, err := groupManager.GetWorkloadGroup(ctx, workloadName)
145-
if err != nil {
146-
return fmt.Errorf("failed to get workload group: %v", err)
147-
}
148-
if group != nil && group.Name != runFlags.Group {
149-
return fmt.Errorf("workload '%s' is already in group '%s'", workloadName, group.Name)
150-
}
151-
152-
// Validate that the group specified exists
153-
exists, err := groupManager.Exists(ctx, runFlags.Group)
154-
if err != nil {
155-
return fmt.Errorf("failed to check if group exists: %v", err)
156-
}
157-
if !exists {
158-
return fmt.Errorf("group '%s' does not exist", runFlags.Group)
159-
}
130+
err := validateGroup(ctx, serverOrImage)
131+
if err != nil {
132+
return err
160133
}
161134

162135
// Build the run configuration
@@ -172,7 +145,7 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
172145
}
173146
workloadManager := workloads.NewManagerFromRuntime(rt)
174147

175-
if isForeground {
148+
if runFlags.Foreground {
176149
return runForeground(ctx, workloadManager, runnerConfig)
177150
}
178151
return workloadManager.RunWorkloadDetached(ctx, runnerConfig)
@@ -203,6 +176,40 @@ func runForeground(ctx context.Context, workloadManager workloads.Manager, runne
203176
}
204177
}
205178

179+
func validateGroup(ctx context.Context, serverOrImage string) error {
180+
workloadName := runFlags.Name
181+
if workloadName == "" {
182+
workloadName = serverOrImage
183+
}
184+
185+
// Create group manager
186+
groupManager, err := groups.NewManager()
187+
if err != nil {
188+
return fmt.Errorf("failed to create group manager: %v", err)
189+
}
190+
191+
// Check if the workload is already in a group
192+
group, err := groupManager.GetWorkloadGroup(ctx, workloadName)
193+
if err != nil {
194+
return fmt.Errorf("failed to get workload group: %v", err)
195+
}
196+
if group != nil && group.Name != runFlags.Group {
197+
return fmt.Errorf("workload '%s' is already in group '%s'", workloadName, group.Name)
198+
}
199+
200+
if runFlags.Group != "" {
201+
// Validate that the group specified exists
202+
exists, err := groupManager.Exists(ctx, runFlags.Group)
203+
if err != nil {
204+
return fmt.Errorf("failed to check if group exists: %v", err)
205+
}
206+
if !exists {
207+
return fmt.Errorf("group '%s' does not exist", runFlags.Group)
208+
}
209+
}
210+
return nil
211+
}
212+
206213
// parseCommandArguments processes command-line arguments to find everything after the -- separator
207214
// which are the arguments to be passed to the MCP server
208215
func parseCommandArguments(args []string) []string {

cmd/thv/app/run_flags.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ func AddRunFlags(cmd *cobra.Command, config *RunFlags) {
8181
cmd.Flags().StringVar(&config.ProxyMode, "proxy-mode", "sse", "Proxy mode for stdio transport (sse or streamable-http)")
8282
cmd.Flags().StringVar(&config.Name, "name", "", "Name of the MCP server (auto-generated from image if not provided)")
8383
// TODO: Re-enable when group functionality is complete
84-
// cmd.Flags().StringVar(&config.Group, "group", "", "Name of the group this workload belongs to")
84+
// cmd.Flags().StringVar(&config.Group, "group", "default",
85+
// "Name of the group this workload belongs to (defaults to 'default' if not specified)")
8586
cmd.Flags().StringVar(&config.Host, "host", transport.LocalhostIPv4, "Host for the HTTP proxy to listen on (IP or hostname)")
8687
cmd.Flags().IntVar(&config.ProxyPort, "proxy-port", 0, "Port for the HTTP proxy to listen on (host port)")
8788
cmd.Flags().IntVar(&config.TargetPort, "target-port", 0,

cmd/thv/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ func main() {
1818
// Handles the auto-discovery flag depreciation, only executes once on old config files
1919
client.CheckAndPerformAutoDiscoveryMigration()
2020

21+
// Check and perform default group migration if needed
22+
// Migrates existing workloads to the default group, only executes once
23+
// TODO: Re-enable when group functionality is complete
24+
// groups.CheckAndPerformDefaultGroupMigration()
25+
2126
// Skip update check for completion command or if we are running in kubernetes
2227
if err := app.NewRootCmd(!app.IsCompletionCommand(os.Args) && !runtime.IsKubernetesRuntime()).Execute(); err != nil {
2328
os.Exit(1)

pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Config struct {
2929
AllowPrivateRegistryIp bool `yaml:"allow_private_registry_ip"`
3030
CACertificatePath string `yaml:"ca_certificate_path,omitempty"`
3131
OTEL OpenTelemetryConfig `yaml:"otel,omitempty"`
32+
DefaultGroupMigration bool `yaml:"default_group_migration,omitempty"`
3233
}
3334

3435
// Secrets contains the settings for secrets management.
@@ -93,6 +94,7 @@ func createNewConfigWithDefaults() Config {
9394
},
9495
RegistryUrl: "",
9596
AllowPrivateRegistryIp: false,
97+
DefaultGroupMigration: false,
9698
}
9799
}
98100

pkg/groups/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import (
1111
"github.com/stacklok/toolhive/pkg/state"
1212
)
1313

14+
const (
15+
// DefaultGroupName is the name of the default group
16+
DefaultGroupName = "default"
17+
)
18+
1419
// manager implements the Manager interface
1520
type manager struct {
1621
groupStore state.Store

pkg/groups/migration.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package groups
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
8+
"github.com/stacklok/toolhive/pkg/config"
9+
"github.com/stacklok/toolhive/pkg/logger"
10+
"github.com/stacklok/toolhive/pkg/runner"
11+
"github.com/stacklok/toolhive/pkg/state"
12+
)
13+
14+
// migrationOnce ensures the migration only runs once
15+
var migrationOnce sync.Once
16+
17+
// CheckAndPerformDefaultGroupMigration checks if default group migration is needed and performs it
18+
// This is called once at application startup
19+
func CheckAndPerformDefaultGroupMigration() {
20+
migrationOnce.Do(func() {
21+
appConfig := config.GetConfig()
22+
23+
// Check if default group migration has already been performed
24+
if appConfig.DefaultGroupMigration {
25+
return
26+
}
27+
28+
performDefaultGroupMigration()
29+
})
30+
}
31+
32+
// performDefaultGroupMigration migrates all existing workloads to the default group
33+
func performDefaultGroupMigration() {
34+
fmt.Println("Migrating existing workloads to default group...")
35+
fmt.Println()
36+
37+
// Create group manager and ensure default group exists
38+
groupManager, err := NewManager()
39+
if err != nil {
40+
logger.Errorf("Failed to create group manager: %v", err)
41+
return
42+
}
43+
44+
// Create default group
45+
if err := createDefaultGroup(context.Background(), groupManager); err != nil {
46+
logger.Errorf("Failed to create default group: %v", err)
47+
return
48+
}
49+
50+
// Create a runconfig store to list all runconfigs
51+
runConfigStore, err := state.NewRunConfigStore("toolhive")
52+
if err != nil {
53+
logger.Errorf("Failed to create runconfig store: %v", err)
54+
return
55+
}
56+
57+
// List all runconfig names
58+
runConfigNames, err := runConfigStore.List(context.Background())
59+
if err != nil {
60+
logger.Errorf("Failed to list runconfigs: %v", err)
61+
return
62+
}
63+
64+
migratedCount := 0
65+
for _, runConfigName := range runConfigNames {
66+
// Load the runconfig
67+
runnerInstance, err := runner.LoadState(context.Background(), runConfigName)
68+
if err != nil {
69+
// Log the error but continue processing other runconfigs
70+
logger.Warnf("Failed to load runconfig %s: %v", runConfigName, err)
71+
continue
72+
}
73+
74+
// If the workload has no group, assign it to the default group
75+
if runnerInstance.Config.Group == "" {
76+
runnerInstance.Config.Group = DefaultGroupName
77+
if err := runnerInstance.SaveState(context.Background()); err != nil {
78+
logger.Warnf("Failed to save runconfig for %s: %v", runConfigName, err)
79+
continue
80+
}
81+
migratedCount++
82+
}
83+
}
84+
85+
if migratedCount > 0 {
86+
fmt.Printf("\nSuccessfully migrated %d workloads to default group '%s'\n", migratedCount, DefaultGroupName)
87+
} else {
88+
fmt.Println("No workloads needed migration to default group")
89+
}
90+
91+
// Mark default group migration as completed
92+
err = config.UpdateConfig(func(c *config.Config) {
93+
c.DefaultGroupMigration = true
94+
})
95+
96+
if err != nil {
97+
logger.Errorf("Error updating config during migration: %v", err)
98+
return
99+
}
100+
}
101+
102+
// createDefaultGroup creates the default group if it doesn't exist
103+
func createDefaultGroup(ctx context.Context, groupManager Manager) error {
104+
logger.Infof("Creating default group '%s'", DefaultGroupName)
105+
if err := groupManager.Create(ctx, DefaultGroupName); err != nil {
106+
return fmt.Errorf("failed to create default group: %w", err)
107+
}
108+
return nil
109+
}

0 commit comments

Comments
 (0)