Skip to content

Commit 981a43a

Browse files
authored
Added apis for exposing servers group feature (#1218)
1 parent 84b9c99 commit 981a43a

File tree

21 files changed

+1279
-112
lines changed

21 files changed

+1279
-112
lines changed

cmd/thv/app/common.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,26 @@ func completeLogsArgs(cmd *cobra.Command, args []string, _ string) ([]string, co
144144

145145
return completions, cobra.ShellCompDirectiveNoFileComp
146146
}
147+
148+
// ValidateGroupFlag returns a cobra PreRunE-compatible function
149+
// that validates the --group flag *if provided*.
150+
/*func validateGroupFlag() func(cmd *cobra.Command, args []string) error {
151+
return func(cmd *cobra.Command, _ []string) error {
152+
groupName, err := cmd.Flags().GetString("group")
153+
if err != nil {
154+
return fmt.Errorf("could not read --group flag: %w", err)
155+
}
156+
157+
if groupName == "" {
158+
// Optional flag not provided — no validation needed
159+
return nil
160+
}
161+
162+
// Validate if provided
163+
if err := validation.ValidateGroupName(groupName); err != nil {
164+
return fmt.Errorf("invalid group name in --group: %w", err)
165+
}
166+
167+
return nil
168+
}
169+
}*/

cmd/thv/app/group.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import (
55
"context"
66
"fmt"
77
"os"
8-
"sort"
98
"strings"
109
"text/tabwriter"
1110

1211
"github.com/spf13/cobra"
1312

1413
"github.com/stacklok/toolhive/pkg/groups"
14+
"github.com/stacklok/toolhive/pkg/validation"
1515
"github.com/stacklok/toolhive/pkg/workloads"
1616
)
1717

@@ -24,9 +24,11 @@ var groupCmd = &cobra.Command{
2424
var groupCreateCmd = &cobra.Command{
2525
Use: "create [group-name]",
2626
Short: "Create a new group of MCP servers",
27-
Long: `Create a new logical group of MCP servers. The group can be used to organize and manage multiple MCP servers together.`,
28-
Args: cobra.ExactArgs(1),
29-
RunE: groupCreateCmdFunc,
27+
Long: `Create a new logical group of MCP servers.
28+
The group can be used to organize and manage multiple MCP servers together.`,
29+
Args: cobra.ExactArgs(1),
30+
PreRunE: validateGroupArg(),
31+
RunE: groupCreateCmdFunc,
3032
}
3133

3234
var groupListCmd = &cobra.Command{
@@ -41,8 +43,21 @@ var groupRmCmd = &cobra.Command{
4143
Short: "Remove a group and remove workloads from it",
4244
Long: "Remove a group and remove all MCP servers from it. By default, this only removes the group " +
4345
"membership from workloads without deleting them. Use --with-workloads to also delete the workloads. ",
44-
Args: cobra.ExactArgs(1),
45-
RunE: groupRmCmdFunc,
46+
Args: cobra.ExactArgs(1),
47+
PreRunE: validateGroupArg(),
48+
RunE: groupRmCmdFunc,
49+
}
50+
51+
func validateGroupArg() func(cmd *cobra.Command, args []string) error {
52+
return func(_ *cobra.Command, args []string) error {
53+
if len(args) == 0 {
54+
return fmt.Errorf("group name is required")
55+
}
56+
if err := validation.ValidateGroupName(args[0]); err != nil {
57+
return fmt.Errorf("invalid group name: %w", err)
58+
}
59+
return nil
60+
}
4661
}
4762

4863
var withWorkloadsFlag bool
@@ -77,11 +92,6 @@ func groupListCmdFunc(cmd *cobra.Command, _ []string) error {
7792
return fmt.Errorf("failed to list groups: %w", err)
7893
}
7994

80-
// Sort groups alphanumerically by name (handles mixed characters, numbers, etc.)
81-
sort.Slice(allGroups, func(i, j int) bool {
82-
return strings.Compare(allGroups[i].Name, allGroups[j].Name) < 0
83-
})
84-
8595
if len(allGroups) == 0 {
8696
fmt.Println("No groups configured.")
8797
return nil
@@ -111,14 +121,13 @@ func groupRmCmdFunc(cmd *cobra.Command, args []string) error {
111121
if strings.EqualFold(groupName, groups.DefaultGroup) {
112122
return fmt.Errorf("cannot delete the %s group", groups.DefaultGroup)
113123
}
114-
115-
groupManager, err := groups.NewManager()
124+
manager, err := groups.NewManager()
116125
if err != nil {
117126
return fmt.Errorf("failed to create group manager: %w", err)
118127
}
119128

120129
// Check if group exists
121-
exists, err := groupManager.Exists(ctx, groupName)
130+
exists, err := manager.Exists(ctx, groupName)
122131
if err != nil {
123132
return fmt.Errorf("failed to check if group exists: %w", err)
124133
}
@@ -127,7 +136,7 @@ func groupRmCmdFunc(cmd *cobra.Command, args []string) error {
127136
}
128137

129138
// Get all workloads in the group
130-
groupWorkloads, err := groupManager.ListWorkloadsInGroup(ctx, groupName)
139+
groupWorkloads, err := manager.ListWorkloadsInGroup(ctx, groupName)
131140
if err != nil {
132141
return fmt.Errorf("failed to list workloads in group: %w", err)
133142
}
@@ -154,7 +163,7 @@ func groupRmCmdFunc(cmd *cobra.Command, args []string) error {
154163
return err
155164
}
156165

157-
if err = groupManager.Delete(ctx, groupName); err != nil {
166+
if err = manager.Delete(ctx, groupName); err != nil {
158167
return fmt.Errorf("failed to delete group: %w", err)
159168
}
160169

cmd/thv/app/list.go

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package app
22

33
import (
4-
"context"
54
"encoding/json"
65
"fmt"
76
"os"
87
"text/tabwriter"
98

109
"github.com/spf13/cobra"
1110

12-
"github.com/stacklok/toolhive/pkg/groups"
1311
"github.com/stacklok/toolhive/pkg/logger"
1412
"github.com/stacklok/toolhive/pkg/workloads"
1513
)
@@ -34,6 +32,8 @@ func init() {
3432
listCmd.Flags().StringArrayVarP(&listLabelFilter, "label", "l", []string{}, "Filter workloads by labels (format: key=value)")
3533
// TODO: Re-enable when group functionality is complete
3634
// listCmd.Flags().StringVar(&listGroupFilter, "group", "", "Filter workloads by group")
35+
36+
// listCmd.PreRunE = validateGroupFlag()
3737
}
3838

3939
func listCmdFunc(cmd *cobra.Command, _ []string) error {
@@ -52,7 +52,7 @@ func listCmdFunc(cmd *cobra.Command, _ []string) error {
5252

5353
// Apply group filtering if specified
5454
if listGroupFilter != "" {
55-
workloadList, err = filterWorkloadsByGroup(ctx, workloadList, listGroupFilter)
55+
workloadList, err = workloads.FilterByGroup(ctx, workloadList, listGroupFilter)
5656
if err != nil {
5757
return fmt.Errorf("failed to filter workloads by group: %v", err)
5858
}
@@ -79,47 +79,6 @@ func listCmdFunc(cmd *cobra.Command, _ []string) error {
7979
}
8080
}
8181

82-
// filterWorkloadsByGroup filters workloads to only include those in the specified group
83-
func filterWorkloadsByGroup(
84-
ctx context.Context, workloadList []workloads.Workload, groupName string,
85-
) ([]workloads.Workload, error) {
86-
// Create group manager
87-
groupManager, err := groups.NewManager()
88-
if err != nil {
89-
return nil, fmt.Errorf("failed to create group manager: %v", err)
90-
}
91-
92-
// Check if the group exists
93-
exists, err := groupManager.Exists(ctx, groupName)
94-
if err != nil {
95-
return nil, fmt.Errorf("failed to check if group exists: %v", err)
96-
}
97-
if !exists {
98-
return nil, fmt.Errorf("group '%s' does not exist", groupName)
99-
}
100-
101-
// Get all workload names in the specified group
102-
groupWorkloadNames, err := groupManager.ListWorkloadsInGroup(ctx, groupName)
103-
if err != nil {
104-
return nil, fmt.Errorf("failed to list workloads in group: %v", err)
105-
}
106-
107-
groupWorkloadMap := make(map[string]struct{})
108-
for _, name := range groupWorkloadNames {
109-
groupWorkloadMap[name] = struct{}{}
110-
}
111-
112-
// Filter workloads that belong to the specified group
113-
var filteredWorkloads []workloads.Workload
114-
for _, workload := range workloadList {
115-
if _, ok := groupWorkloadMap[workload.Name]; ok {
116-
filteredWorkloads = append(filteredWorkloads, workload)
117-
}
118-
}
119-
120-
return filteredWorkloads, nil
121-
}
122-
12382
// printJSONOutput prints workload information in JSON format
12483
func printJSONOutput(workloadList []workloads.Workload) error {
12584
// Marshal to JSON

cmd/thv/app/restart.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ func init() {
3232
//
3333
//// Mark the flags as mutually exclusive
3434
//restartCmd.MarkFlagsMutuallyExclusive("all", "group")
35+
36+
// restartCmd.PreRunE = validateGroupFlag()
3537
}
3638

3739
func restartCmdFunc(cmd *cobra.Command, args []string) error {

cmd/thv/app/rm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ var rmCmd = &cobra.Command{
2222
func init() {
2323
// TODO: Re-enable when group functionality is complete
2424
// rmCmd.Flags().String("group", "", "Delete all workloads in the specified group")
25+
26+
// rmCmd.PreRunE = validateGroupFlag()
2527
}
2628

2729
//nolint:gocyclo // This function is complex but manageable

cmd/thv/app/run.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ func init() {
7272
// Add run flags
7373
AddRunFlags(runCmd, &runFlags)
7474

75+
//runCmd.PreRunE = validateGroupFlag()
76+
7577
// This is used for the K8s operator which wraps the run command, but shouldn't be visible to users.
7678
if err := runCmd.Flags().MarkHidden("k8s-pod-patch"); err != nil {
7779
logger.Warnf("Error hiding flag: %v", err)

docs/server/docs.go

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

docs/server/swagger.json

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

0 commit comments

Comments
 (0)