Skip to content

Commit 0361ffb

Browse files
authored
Add group flag to client removal (#1371)
1 parent 6c3b237 commit 0361ffb

File tree

10 files changed

+455
-89
lines changed

10 files changed

+455
-89
lines changed

cmd/thv/app/client.go

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/stacklok/toolhive/pkg/config"
1313
"github.com/stacklok/toolhive/pkg/core"
1414
"github.com/stacklok/toolhive/pkg/groups"
15+
"github.com/stacklok/toolhive/pkg/logger"
1516
"github.com/stacklok/toolhive/pkg/workloads"
1617
)
1718

@@ -104,6 +105,8 @@ func init() {
104105
// TODO: Re-enable when group functionality is complete
105106
//clientRegisterCmd.Flags().StringSliceVar(
106107
// &groupNames, "group", []string{"default"}, "Only register workloads from specified groups")
108+
//clientRemoveCmd.Flags().StringSliceVar(
109+
// &groupNames, "group", []string{}, "Remove client from specified groups (if not set, removes all workloads from the client)")
107110
}
108111

109112
func clientStatusCmdFunc(_ *cobra.Command, _ []string) error {
@@ -208,21 +211,7 @@ func clientRemoveCmdFunc(cmd *cobra.Command, args []string) error {
208211
clientType)
209212
}
210213

211-
ctx := cmd.Context()
212-
213-
manager, err := client.NewManager(ctx)
214-
if err != nil {
215-
return fmt.Errorf("failed to create client manager: %w", err)
216-
}
217-
218-
err = manager.UnregisterClients(ctx, []client.Client{
219-
{Name: client.MCPClient(clientType)},
220-
})
221-
if err != nil {
222-
return fmt.Errorf("failed to remove client %s: %w", clientType, err)
223-
}
224-
225-
return nil
214+
return performClientRemoval(cmd.Context(), client.Client{Name: client.MCPClient(clientType)}, groupNames)
226215
}
227216

228217
func listRegisteredClientsCmdFunc(_ *cobra.Command, _ []string) error {
@@ -339,3 +328,114 @@ func registerClientsGlobally(
339328

340329
return nil
341330
}
331+
332+
func performClientRemoval(ctx context.Context, clientToRemove client.Client, groupNames []string) error {
333+
clientManager, err := client.NewManager(ctx)
334+
if err != nil {
335+
return fmt.Errorf("failed to create client manager: %w", err)
336+
}
337+
338+
workloadManager, err := workloads.NewManager(ctx)
339+
if err != nil {
340+
return fmt.Errorf("failed to create workload manager: %w", err)
341+
}
342+
343+
runningWorkloads, err := workloadManager.ListWorkloads(ctx, false)
344+
if err != nil {
345+
return fmt.Errorf("failed to list running workloads: %w", err)
346+
}
347+
348+
groupManager, err := groups.NewManager()
349+
if err != nil {
350+
return fmt.Errorf("failed to create group manager: %w", err)
351+
}
352+
353+
if len(groupNames) > 0 {
354+
return removeClientFromGroups(ctx, clientToRemove, groupNames, runningWorkloads, groupManager, clientManager)
355+
}
356+
357+
return removeClientGlobally(ctx, clientToRemove, runningWorkloads, groupManager, clientManager)
358+
}
359+
360+
func removeClientFromGroups(
361+
ctx context.Context,
362+
clientToRemove client.Client,
363+
groupNames []string,
364+
runningWorkloads []core.Workload,
365+
groupManager groups.Manager,
366+
clientManager client.Manager,
367+
) error {
368+
fmt.Printf("Filtering workloads to groups: %v\n", groupNames)
369+
370+
// Remove client from specific groups only
371+
filteredWorkloads, err := workloads.FilterByGroups(runningWorkloads, groupNames)
372+
if err != nil {
373+
return fmt.Errorf("failed to filter workloads by groups: %w", err)
374+
}
375+
376+
// Remove the workloads from the client's configuration file
377+
err = clientManager.UnregisterClients(ctx, []client.Client{clientToRemove}, filteredWorkloads)
378+
if err != nil {
379+
return fmt.Errorf("failed to unregister client: %w", err)
380+
}
381+
382+
// Remove the client from the groups
383+
err = groupManager.UnregisterClients(ctx, groupNames, []string{string(clientToRemove.Name)})
384+
if err != nil {
385+
return fmt.Errorf("failed to unregister client from groups: %w", err)
386+
}
387+
388+
fmt.Printf("Successfully removed client %s from groups: %v\n", clientToRemove.Name, groupNames)
389+
390+
return nil
391+
}
392+
393+
func removeClientGlobally(
394+
ctx context.Context,
395+
clientToRemove client.Client,
396+
runningWorkloads []core.Workload,
397+
groupManager groups.Manager,
398+
clientManager client.Manager,
399+
) error {
400+
// Remove the workloads from the client's configuration file
401+
err := clientManager.UnregisterClients(ctx, []client.Client{clientToRemove}, runningWorkloads)
402+
if err != nil {
403+
return fmt.Errorf("failed to unregister client: %w", err)
404+
}
405+
406+
allGroups, err := groupManager.List(ctx)
407+
if err != nil {
408+
return fmt.Errorf("failed to list groups: %w", err)
409+
}
410+
411+
if len(allGroups) > 0 {
412+
// Remove client from all groups first
413+
allGroupNames := make([]string, len(allGroups))
414+
for i, group := range allGroups {
415+
allGroupNames[i] = group.Name
416+
}
417+
418+
err = groupManager.UnregisterClients(ctx, allGroupNames, []string{string(clientToRemove.Name)})
419+
if err != nil {
420+
return fmt.Errorf("failed to unregister client from groups: %w", err)
421+
}
422+
}
423+
424+
// Remove client from global registered clients list
425+
err = config.UpdateConfig(func(c *config.Config) {
426+
for i, registeredClient := range c.Clients.RegisteredClients {
427+
if registeredClient == string(clientToRemove.Name) {
428+
// Remove client from slice
429+
c.Clients.RegisteredClients = append(c.Clients.RegisteredClients[:i], c.Clients.RegisteredClients[i+1:]...)
430+
logger.Infof("Successfully unregistered client: %s\n", clientToRemove.Name)
431+
return
432+
}
433+
}
434+
logger.Warnf("Client %s was not found in registered clients list", clientToRemove.Name)
435+
})
436+
if err != nil {
437+
return fmt.Errorf("failed to update configuration for client %s: %w", clientToRemove.Name, err)
438+
}
439+
440+
return nil
441+
}

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: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/v1/clients.go

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/stacklok/toolhive/pkg/client"
1212
"github.com/stacklok/toolhive/pkg/config"
13+
"github.com/stacklok/toolhive/pkg/core"
1314
"github.com/stacklok/toolhive/pkg/groups"
1415
"github.com/stacklok/toolhive/pkg/logger"
1516
"github.com/stacklok/toolhive/pkg/workloads"
@@ -38,6 +39,7 @@ func ClientRouter(
3839
r.Get("/", routes.listClients)
3940
r.Post("/", routes.registerClient)
4041
r.Delete("/{name}", routes.unregisterClient)
42+
r.Delete("/{name}/groups/{group}", routes.unregisterClientFromGroup)
4143
r.Post("/register", routes.registerClientsBulk)
4244
r.Post("/unregister", routes.unregisterClientsBulk)
4345
return r
@@ -129,9 +131,7 @@ func (c *ClientRoutes) unregisterClient(w http.ResponseWriter, r *http.Request)
129131
return
130132
}
131133

132-
err := c.clientManager.UnregisterClients(r.Context(), []client.Client{
133-
{Name: client.MCPClient(clientName)},
134-
})
134+
err := c.removeClient(r.Context(), []client.Client{{Name: client.MCPClient(clientName)}}, nil)
135135
if err != nil {
136136
logger.Errorf("Failed to unregister client: %v", err)
137137
http.Error(w, "Failed to unregister client", http.StatusInternalServerError)
@@ -141,6 +141,41 @@ func (c *ClientRoutes) unregisterClient(w http.ResponseWriter, r *http.Request)
141141
w.WriteHeader(http.StatusNoContent)
142142
}
143143

144+
// unregisterClientFromGroup
145+
//
146+
// @Summary Unregister a client from a specific group
147+
// @Description Unregister a client from a specific group in ToolHive
148+
// @Tags clients
149+
// @Param name path string true "Client name to unregister"
150+
// @Param group path string true "Group name to remove client from"
151+
// @Success 204
152+
// @Failure 400 {string} string "Invalid request"
153+
// @Failure 404 {string} string "Client or group not found"
154+
// @Router /api/v1beta/clients/{name}/groups/{group} [delete]
155+
func (c *ClientRoutes) unregisterClientFromGroup(w http.ResponseWriter, r *http.Request) {
156+
clientName := chi.URLParam(r, "name")
157+
if clientName == "" {
158+
http.Error(w, "Client name is required", http.StatusBadRequest)
159+
return
160+
}
161+
162+
groupName := chi.URLParam(r, "group")
163+
if groupName == "" {
164+
http.Error(w, "Group name is required", http.StatusBadRequest)
165+
return
166+
}
167+
168+
// Remove client from the specific group
169+
err := c.removeClient(r.Context(), []client.Client{{Name: client.MCPClient(clientName)}}, []string{groupName})
170+
if err != nil {
171+
logger.Errorf("Failed to unregister client from group: %v", err)
172+
http.Error(w, "Failed to unregister client from group", http.StatusInternalServerError)
173+
return
174+
}
175+
176+
w.WriteHeader(http.StatusNoContent)
177+
}
178+
144179
// registerClientsBulk
145180
//
146181
// @Summary Register multiple clients
@@ -220,7 +255,7 @@ func (c *ClientRoutes) unregisterClientsBulk(w http.ResponseWriter, r *http.Requ
220255
clients[i] = client.Client{Name: name}
221256
}
222257

223-
err = c.clientManager.UnregisterClients(r.Context(), clients)
258+
err = c.removeClient(r.Context(), clients, req.Groups)
224259
if err != nil {
225260
logger.Errorf("Failed to unregister clients: %v", err)
226261
http.Error(w, "Failed to unregister clients", http.StatusInternalServerError)
@@ -310,3 +345,104 @@ func (c *ClientRoutes) performClientRegistration(ctx context.Context, clients []
310345

311346
return nil
312347
}
348+
349+
func (c *ClientRoutes) removeClient(ctx context.Context, clients []client.Client, groupNames []string) error {
350+
runningWorkloads, err := c.workloadManager.ListWorkloads(ctx, false)
351+
if err != nil {
352+
return fmt.Errorf("failed to list running workloads: %w", err)
353+
}
354+
355+
if len(groupNames) > 0 {
356+
return c.removeClientFromGroup(ctx, clients, groupNames, runningWorkloads)
357+
}
358+
359+
return c.removeClientGlobally(ctx, clients, runningWorkloads)
360+
}
361+
362+
func (c *ClientRoutes) removeClientFromGroup(
363+
ctx context.Context,
364+
clients []client.Client,
365+
groupNames []string,
366+
runningWorkloads []core.Workload,
367+
) error {
368+
// Remove clients from specific groups only
369+
filteredWorkloads, err := workloads.FilterByGroups(runningWorkloads, groupNames)
370+
if err != nil {
371+
return fmt.Errorf("failed to filter workloads by groups: %w", err)
372+
}
373+
374+
// Remove the workloads from the client's configuration file
375+
err = c.clientManager.UnregisterClients(ctx, clients, filteredWorkloads)
376+
if err != nil {
377+
return fmt.Errorf("failed to unregister clients: %w", err)
378+
}
379+
380+
// Extract client names for group management
381+
clientNames := make([]string, len(clients))
382+
for i, clientToRemove := range clients {
383+
clientNames[i] = string(clientToRemove.Name)
384+
}
385+
386+
// Remove the clients from the groups
387+
err = c.groupManager.UnregisterClients(ctx, groupNames, clientNames)
388+
if err != nil {
389+
return fmt.Errorf("failed to unregister clients from groups: %w", err)
390+
}
391+
392+
return nil
393+
}
394+
395+
func (c *ClientRoutes) removeClientGlobally(
396+
ctx context.Context,
397+
clients []client.Client,
398+
runningWorkloads []core.Workload,
399+
) error {
400+
// Remove the workloads from the client's configuration file
401+
err := c.clientManager.UnregisterClients(ctx, clients, runningWorkloads)
402+
if err != nil {
403+
return fmt.Errorf("failed to unregister clients: %w", err)
404+
}
405+
406+
// Remove clients from all groups and global registry
407+
allGroups, err := c.groupManager.List(ctx)
408+
if err != nil {
409+
return fmt.Errorf("failed to list groups: %w", err)
410+
}
411+
412+
if len(allGroups) > 0 {
413+
clientNames := make([]string, len(clients))
414+
for i, clientToRemove := range clients {
415+
clientNames[i] = string(clientToRemove.Name)
416+
}
417+
418+
allGroupNames := make([]string, len(allGroups))
419+
for i, group := range allGroups {
420+
allGroupNames[i] = group.Name
421+
}
422+
423+
err = c.groupManager.UnregisterClients(ctx, allGroupNames, clientNames)
424+
if err != nil {
425+
return fmt.Errorf("failed to unregister clients from groups: %w", err)
426+
}
427+
}
428+
429+
// Remove clients from global registered clients list
430+
for _, clientToRemove := range clients {
431+
err := config.UpdateConfig(func(c *config.Config) {
432+
for i, registeredClient := range c.Clients.RegisteredClients {
433+
if registeredClient == string(clientToRemove.Name) {
434+
// Remove client from slice
435+
c.Clients.RegisteredClients = append(c.Clients.RegisteredClients[:i], c.Clients.RegisteredClients[i+1:]...)
436+
logger.Infof("Successfully unregistered client: %s\n", clientToRemove.Name)
437+
return
438+
}
439+
}
440+
logger.Warnf("Client %s was not found in registered clients list", clientToRemove.Name)
441+
})
442+
if err != nil {
443+
return fmt.Errorf("failed to update configuration for client %s: %w", clientToRemove.Name, err)
444+
}
445+
}
446+
447+
return nil
448+
}

0 commit comments

Comments
 (0)