Skip to content

Commit 77e3225

Browse files
authored
Migrate clients to default group (#1265)
1 parent 5fe00a8 commit 77e3225

File tree

9 files changed

+210
-11
lines changed

9 files changed

+210
-11
lines changed

docs/server/docs.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.yaml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/v1/groups_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ func TestGroupsRouter(t *testing.T) {
4040
path: "/",
4141
setupMock: func(m *mocks.MockManager) {
4242
m.EXPECT().List(gomock.Any()).Return([]*groups.Group{
43-
{Name: "group1"},
44-
{Name: "group2"},
43+
{Name: "group1", RegisteredClients: []string{}},
44+
{Name: "group2", RegisteredClients: []string{}},
4545
}, nil)
4646
},
4747
expectedStatus: http.StatusOK,
48-
expectedBody: `{"groups":[{"name":"group1"},{"name":"group2"}]}`,
48+
expectedBody: `{"groups":[{"name":"group1", "registered_clients": []},{"name":"group2", "registered_clients": []}]}`,
4949
},
5050
{
5151
name: "list groups error",
@@ -106,10 +106,11 @@ func TestGroupsRouter(t *testing.T) {
106106
method: "GET",
107107
path: "/testgroup",
108108
setupMock: func(m *mocks.MockManager) {
109-
m.EXPECT().Get(gomock.Any(), "testgroup").Return(&groups.Group{Name: "testgroup"}, nil)
109+
m.EXPECT().Get(gomock.Any(), "testgroup").
110+
Return(&groups.Group{Name: "testgroup", RegisteredClients: []string{}}, nil)
110111
},
111112
expectedStatus: http.StatusOK,
112-
expectedBody: `{"name":"testgroup"}`,
113+
expectedBody: `{"name":"testgroup", "registered_clients": []}`,
113114
},
114115
{
115116
name: "get group not found",

pkg/groups/group.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const DefaultGroup = "default"
1313

1414
// Group represents a logical grouping of MCP servers.
1515
type Group struct {
16-
Name string `json:"name"`
16+
Name string `json:"name"`
17+
RegisteredClients []string `json:"registered_clients"`
1718
}
1819

1920
// WriteJSON serializes the Group to JSON and writes it to the provided writer
@@ -52,4 +53,7 @@ type Manager interface {
5253

5354
// ListWorkloadsInGroup returns all workload names that belong to the specified group.
5455
ListWorkloadsInGroup(ctx context.Context, groupName string) ([]string, error)
56+
57+
// RegisterClient registers a client with the group.
58+
RegisterClient(ctx context.Context, groupName, clientName string) error
5559
}

pkg/groups/group_test.go

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ func TestManager_Get(t *testing.T) {
127127
name: "successful retrieval",
128128
groupName: testGroupName,
129129
setupMock: func(mock *mocks.MockStore) {
130-
groupData := `{"name": "` + testGroupName + `"}`
130+
groupData := `{"name": "` + testGroupName + `", "registered_clients": ["client1", "client2"]}`
131131
mock.EXPECT().
132132
GetReader(gomock.Any(), testGroupName).
133133
Return(io.NopCloser(strings.NewReader(groupData)), nil)
134134
},
135135
expectError: false,
136-
expectedGroup: &Group{Name: testGroupName},
136+
expectedGroup: &Group{Name: testGroupName, RegisteredClients: []string{"client1", "client2"}},
137137
},
138138
{
139139
name: "group not found",
@@ -183,6 +183,7 @@ func TestManager_Get(t *testing.T) {
183183
} else {
184184
assert.NoError(t, err)
185185
assert.Equal(t, tt.expectedGroup.Name, group.Name)
186+
assert.Equal(t, tt.expectedGroup.RegisteredClients, group.RegisteredClients)
186187
}
187188
})
188189
}
@@ -503,6 +504,91 @@ func TestManager_GetWorkloadGroup(t *testing.T) {
503504
}
504505
}
505506

507+
// TestManager_RegisterClient tests client registration
508+
func TestManager_RegisterClient(t *testing.T) {
509+
t.Parallel()
510+
511+
tests := []struct {
512+
name string
513+
groupName string
514+
clientName string
515+
setupMock func(*mocks.MockStore)
516+
expectError bool
517+
errorMsg string
518+
}{
519+
{
520+
name: "successful client registration",
521+
groupName: testGroupName,
522+
clientName: "test-client",
523+
setupMock: func(mock *mocks.MockStore) {
524+
// First call to Get() - return existing group
525+
groupData := `{"name": "` + testGroupName + `", "registered_clients": []}`
526+
mock.EXPECT().
527+
GetReader(gomock.Any(), testGroupName).
528+
Return(io.NopCloser(strings.NewReader(groupData)), nil)
529+
530+
// Second call to saveGroup()
531+
mock.EXPECT().
532+
GetWriter(gomock.Any(), testGroupName).
533+
Return(&mockWriteCloser{}, nil)
534+
},
535+
expectError: false,
536+
},
537+
{
538+
name: "client already registered",
539+
groupName: testGroupName,
540+
clientName: "existing-client",
541+
setupMock: func(mock *mocks.MockStore) {
542+
// Return group with client already registered
543+
groupData := `{"name": "` + testGroupName + `", "registered_clients": ["existing-client"]}`
544+
mock.EXPECT().
545+
GetReader(gomock.Any(), testGroupName).
546+
Return(io.NopCloser(strings.NewReader(groupData)), nil)
547+
},
548+
expectError: true,
549+
errorMsg: "is already registered",
550+
},
551+
{
552+
name: "group not found",
553+
groupName: "nonexistent-group",
554+
clientName: "test-client",
555+
setupMock: func(mock *mocks.MockStore) {
556+
mock.EXPECT().
557+
GetReader(gomock.Any(), "nonexistent-group").
558+
Return(nil, &os.PathError{Op: "open", Path: "nonexistent-group", Err: os.ErrNotExist})
559+
},
560+
expectError: true,
561+
errorMsg: "failed to get group",
562+
},
563+
}
564+
565+
for _, tt := range tests {
566+
t.Run(tt.name, func(t *testing.T) {
567+
t.Parallel()
568+
569+
ctrl := gomock.NewController(t)
570+
defer ctrl.Finish()
571+
572+
mockStore := mocks.NewMockStore(ctrl)
573+
manager := &manager{groupStore: mockStore}
574+
575+
// Set up mock expectations
576+
tt.setupMock(mockStore)
577+
578+
// Execute operation
579+
err := manager.RegisterClient(context.Background(), tt.groupName, tt.clientName)
580+
581+
// Verify results
582+
if tt.expectError {
583+
assert.Error(t, err)
584+
assert.Contains(t, err.Error(), tt.errorMsg)
585+
} else {
586+
assert.NoError(t, err)
587+
}
588+
})
589+
}
590+
}
591+
506592
// mockWriteCloser implements io.WriteCloser for testing
507593
type mockWriteCloser struct {
508594
data []byte

pkg/groups/manager.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ func (m *manager) Create(ctx context.Context, name string) error {
4949
return thverrors.NewGroupAlreadyExistsError(fmt.Sprintf("group '%s' already exists", name), nil)
5050
}
5151

52-
group := &Group{Name: name}
52+
group := &Group{
53+
Name: name,
54+
RegisteredClients: []string{},
55+
}
5356
return m.saveGroup(ctx, group)
5457
}
5558

@@ -150,6 +153,28 @@ func (m *manager) ListWorkloadsInGroup(ctx context.Context, groupName string) ([
150153
return groupWorkloads, nil
151154
}
152155

156+
// RegisterClient registers a client with the group
157+
func (m *manager) RegisterClient(ctx context.Context, groupName, clientName string) error {
158+
// Get the existing group
159+
group, err := m.Get(ctx, groupName)
160+
if err != nil {
161+
return fmt.Errorf("failed to get group %s: %w", groupName, err)
162+
}
163+
164+
// Check if client is already registered
165+
for _, existingClient := range group.RegisteredClients {
166+
if existingClient == clientName {
167+
return fmt.Errorf("client '%s' is already registered with group '%s'", clientName, groupName)
168+
}
169+
}
170+
171+
// Add the client to the group
172+
group.RegisteredClients = append(group.RegisteredClients, clientName)
173+
174+
// Save the updated group
175+
return m.saveGroup(ctx, group)
176+
}
177+
153178
// saveGroup saves the group to the group state store
154179
func (m *manager) saveGroup(ctx context.Context, group *Group) error {
155180
writer, err := m.groupStore.GetWriter(ctx, group.Name)

pkg/groups/migration.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ func performDefaultGroupMigration() {
8888
fmt.Println("No workloads needed migration to default group")
8989
}
9090

91+
// Migrate client configurations from global config to default group
92+
if err := migrateClientConfigs(context.Background(), groupManager); err != nil {
93+
logger.Errorf("Failed to migrate client configurations: %v", err)
94+
return
95+
}
96+
9197
// Mark default group migration as completed
9298
err = config.UpdateConfig(func(c *config.Config) {
9399
c.DefaultGroupMigration = true
@@ -99,6 +105,64 @@ func performDefaultGroupMigration() {
99105
}
100106
}
101107

108+
// migrateClientConfigs migrates client configurations from global config to default group
109+
func migrateClientConfigs(ctx context.Context, groupManager Manager) error {
110+
appConfig := config.GetConfig()
111+
112+
// If there are no registered clients, nothing to migrate
113+
if len(appConfig.Clients.RegisteredClients) == 0 {
114+
logger.Infof("No client configurations to migrate")
115+
return nil
116+
}
117+
118+
fmt.Printf("Migrating %d client configurations to default group...\n", len(appConfig.Clients.RegisteredClients))
119+
120+
// Get the default group
121+
defaultGroup, err := groupManager.Get(ctx, DefaultGroupName)
122+
if err != nil {
123+
return fmt.Errorf("failed to get default group: %w", err)
124+
}
125+
126+
migratedCount := 0
127+
// Copy all registered clients to the default group
128+
for _, clientName := range appConfig.Clients.RegisteredClients {
129+
// Check if client is already in the group (avoid duplicates)
130+
alreadyRegistered := false
131+
for _, existingClient := range defaultGroup.RegisteredClients {
132+
if existingClient == clientName {
133+
alreadyRegistered = true
134+
break
135+
}
136+
}
137+
138+
if !alreadyRegistered {
139+
if err := groupManager.RegisterClient(ctx, DefaultGroupName, clientName); err != nil {
140+
logger.Warnf("Failed to register client %s to default group: %v", clientName, err)
141+
continue
142+
}
143+
migratedCount++
144+
}
145+
}
146+
147+
if migratedCount > 0 {
148+
fmt.Printf("Successfully migrated %d client configurations to default group '%s'\n", migratedCount, DefaultGroupName)
149+
150+
// Clear the global client configurations after successful migration
151+
err = config.UpdateConfig(func(c *config.Config) {
152+
c.Clients.RegisteredClients = []string{}
153+
})
154+
if err != nil {
155+
logger.Warnf("Failed to clear global client configurations after migration: %v", err)
156+
} else {
157+
logger.Infof("Cleared global client configurations")
158+
}
159+
} else {
160+
logger.Infof("No client configurations needed migration")
161+
}
162+
163+
return nil
164+
}
165+
102166
// createDefaultGroup creates the default group if it doesn't exist
103167
func createDefaultGroup(ctx context.Context, groupManager Manager) error {
104168
logger.Infof("Creating default group '%s'", DefaultGroupName)

pkg/groups/mocks/mock_manager.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)