feat(cluster): add label selector support to cluster list

Adds --selector/-l flag to argocd cluster list to filter clusters by
label. The selector is passed to the server which filters using
Kubernetes label selector syntax (e.g. env=prod,tier!=frontend).

Signed-off-by: Fernando Torres <nandotorres@gmail.com>
This commit is contained in:
Fernando Torres 2026-04-17 17:36:44 +02:00 committed by Fernando Torres
parent 37e10dba75
commit 811ef8df73
No known key found for this signature in database
GPG key ID: 1C3D323A5ED16732
7 changed files with 248 additions and 48 deletions

18
assets/swagger.json generated
View file

@ -2491,6 +2491,12 @@
"description": "value holds the cluster server URL or cluster name.",
"name": "id.value",
"in": "query"
},
{
"type": "string",
"description": "the selector to restrict returned list to clusters only with matched labels.",
"name": "selector",
"in": "query"
}
],
"responses": {
@ -2575,6 +2581,12 @@
"description": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" ).",
"name": "id.type",
"in": "query"
},
{
"type": "string",
"description": "the selector to restrict returned list to clusters only with matched labels.",
"name": "selector",
"in": "query"
}
],
"responses": {
@ -2674,6 +2686,12 @@
"description": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" ).",
"name": "id.type",
"in": "query"
},
{
"type": "string",
"description": "the selector to restrict returned list to clusters only with matched labels.",
"name": "selector",
"in": "query"
}
],
"responses": {

View file

@ -501,6 +501,7 @@ func printClusterServers(clusters []argoappv1.Cluster) {
// NewClusterListCommand returns a new instance of an `argocd cluster rm` command
func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var output string
var selector string
command := &cobra.Command{
Use: "list",
Short: "List configured clusters",
@ -509,7 +510,7 @@ func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
defer utilio.Close(conn)
clusters, err := clusterIf.List(ctx, &clusterpkg.ClusterQuery{})
clusters, err := clusterIf.List(ctx, &clusterpkg.ClusterQuery{Selector: selector})
errors.CheckError(err)
switch output {
case "yaml", "json":
@ -542,6 +543,7 @@ argocd cluster list -o server <ARGOCD_SERVER_ADDRESS>
`,
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|server")
command.Flags().StringVarP(&selector, "selector", "l", "", "Label selector to filter clusters (e.g., 'env=production,tier!=frontend')")
return command
}

View file

@ -33,8 +33,9 @@ argocd cluster list -o server <ARGOCD_SERVER_ADDRESS>
### Options
```
-h, --help help for list
-o, --output string Output format. One of: json|yaml|wide|server (default "wide")
-h, --help help for list
-o, --output string Output format. One of: json|yaml|wide|server (default "wide")
-l, --selector string Label selector to filter clusters (e.g., 'env=production,tier!=frontend')
```
### Options inherited from parent commands

View file

@ -92,12 +92,14 @@ func (m *ClusterID) GetValue() string {
// ClusterQuery is a query for cluster resources
type ClusterQuery struct {
Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Id *ClusterID `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Id *ClusterID `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
// the selector to restrict returned list to clusters only with matched labels
Selector string `protobuf:"bytes,4,opt,name=selector,proto3" json:"selector,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ClusterQuery) Reset() { *m = ClusterQuery{} }
@ -154,6 +156,13 @@ func (m *ClusterQuery) GetId() *ClusterID {
return nil
}
func (m *ClusterQuery) GetSelector() string {
if m != nil {
return m.Selector
}
return ""
}
type ClusterResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
@ -322,45 +331,46 @@ func init() {
func init() { proto.RegisterFile("server/cluster/cluster.proto", fileDescriptor_a6b5ba0b5aa57b32) }
var fileDescriptor_a6b5ba0b5aa57b32 = []byte{
// 596 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x95, 0xcf, 0x6f, 0xd3, 0x30,
0x14, 0xc7, 0xe5, 0x6e, 0x74, 0xcc, 0x03, 0x06, 0xd6, 0x40, 0x51, 0xf7, 0x43, 0x25, 0x20, 0x28,
0x68, 0xb5, 0xd5, 0x76, 0x5c, 0xb8, 0xb1, 0x0e, 0x50, 0xa5, 0x5d, 0x08, 0xe2, 0xc2, 0x61, 0x93,
0x97, 0x3c, 0xa5, 0x66, 0x59, 0x62, 0x62, 0x27, 0xd2, 0x84, 0xb8, 0xec, 0xc4, 0x0d, 0x21, 0xae,
0x5c, 0xf9, 0x43, 0xb8, 0x21, 0x71, 0x41, 0xe2, 0x1f, 0x40, 0x15, 0x7f, 0x08, 0x8a, 0x93, 0xb4,
0xb4, 0xd3, 0xaa, 0x21, 0x15, 0x4e, 0xf5, 0x7b, 0xea, 0xf3, 0xf7, 0xf3, 0xbe, 0x7e, 0x8e, 0xf1,
0x9a, 0x82, 0x38, 0x85, 0x98, 0xb9, 0x41, 0xa2, 0xf4, 0xe8, 0x97, 0xca, 0x38, 0xd2, 0x11, 0x59,
0x28, 0xc2, 0xda, 0x9a, 0x1f, 0x45, 0x7e, 0x00, 0x8c, 0x4b, 0xc1, 0x78, 0x18, 0x46, 0x9a, 0x6b,
0x11, 0x85, 0x2a, 0xff, 0x5b, 0x6d, 0xd7, 0x17, 0xba, 0x9f, 0x1c, 0x50, 0x37, 0x3a, 0x62, 0x3c,
0xf6, 0x23, 0x19, 0x47, 0xaf, 0xcc, 0xa2, 0xe9, 0x7a, 0x2c, 0xed, 0x30, 0x79, 0xe8, 0x67, 0x95,
0x8a, 0x71, 0x29, 0x03, 0xe1, 0x9a, 0x5a, 0x96, 0xb6, 0x78, 0x20, 0xfb, 0xbc, 0xc5, 0x7c, 0x08,
0x21, 0xe6, 0x1a, 0xbc, 0x7c, 0x37, 0xfb, 0x01, 0x5e, 0xec, 0xe6, 0xb2, 0xbd, 0x1d, 0x42, 0xf0,
0xbc, 0x3e, 0x96, 0x60, 0xa1, 0x3a, 0x6a, 0x2c, 0x3a, 0x66, 0x4d, 0x56, 0xf0, 0x85, 0x94, 0x07,
0x09, 0x58, 0x15, 0x93, 0xcc, 0x03, 0x7b, 0x0f, 0x5f, 0x2a, 0xca, 0x9e, 0x25, 0x10, 0x1f, 0x93,
0x1b, 0xb8, 0x9a, 0xf7, 0x56, 0xd4, 0x16, 0x51, 0xb6, 0x63, 0xc8, 0x8f, 0xca, 0x62, 0xb3, 0x26,
0x36, 0xae, 0x08, 0xcf, 0x9a, 0xab, 0xa3, 0xc6, 0x52, 0x9b, 0xd0, 0xd2, 0x83, 0x21, 0x85, 0x53,
0x11, 0x9e, 0x7d, 0x0d, 0x2f, 0x17, 0x09, 0x07, 0x94, 0x8c, 0x42, 0x05, 0xf6, 0x7b, 0x84, 0x57,
0x8a, 0x5c, 0x37, 0x06, 0xae, 0xc1, 0x81, 0xd7, 0x09, 0x28, 0x4d, 0xf6, 0x71, 0xe9, 0x9c, 0x11,
0x5f, 0x6a, 0x3f, 0xa6, 0x23, 0x8b, 0x68, 0x69, 0x91, 0x59, 0xec, 0xbb, 0x1e, 0x4d, 0x3b, 0x54,
0x1e, 0xfa, 0x34, 0xb3, 0x88, 0xfe, 0x61, 0x11, 0x2d, 0x2d, 0x2a, 0x49, 0x9c, 0x72, 0xd7, 0xac,
0xb9, 0x44, 0x2a, 0x88, 0xb5, 0x69, 0xe3, 0xa2, 0x53, 0x44, 0xf6, 0x97, 0x11, 0xd1, 0x0b, 0xe9,
0xfd, 0x4f, 0xa2, 0xdb, 0xf8, 0x72, 0x62, 0x14, 0xbd, 0x27, 0x02, 0x02, 0x4f, 0x59, 0x95, 0xfa,
0x5c, 0x63, 0xd1, 0x19, 0x4f, 0x9e, 0xc7, 0xe8, 0xf6, 0xb7, 0x05, 0x7c, 0xa5, 0xc8, 0x3c, 0x87,
0x38, 0x15, 0x2e, 0x90, 0x13, 0x84, 0xe7, 0x77, 0x85, 0xd2, 0xe4, 0xfa, 0x64, 0x8d, 0x39, 0xeb,
0x5a, 0x6f, 0x26, 0xcd, 0x64, 0x0a, 0xb6, 0x75, 0xf2, 0xe3, 0xd7, 0xc7, 0x0a, 0x21, 0x57, 0xcd,
0xac, 0xa7, 0xad, 0xf2, 0x46, 0x28, 0xf2, 0x01, 0xe1, 0x6a, 0x7e, 0xcc, 0x64, 0x7d, 0x12, 0x63,
0xec, 0xf8, 0x6b, 0xb3, 0xf1, 0xd6, 0xbe, 0x69, 0x50, 0x56, 0xed, 0x53, 0x28, 0x0f, 0x87, 0xae,
0xbf, 0x43, 0x78, 0xee, 0x29, 0x9c, 0xe9, 0xcb, 0x8c, 0x40, 0x6e, 0x19, 0x90, 0x75, 0xb2, 0x3a,
0x09, 0xc2, 0xde, 0x08, 0x8f, 0x9a, 0xeb, 0xf7, 0x96, 0x7c, 0x42, 0xb8, 0x9a, 0xcf, 0xdc, 0x69,
0x7b, 0xc6, 0x66, 0x71, 0x56, 0x54, 0x9b, 0x86, 0xea, 0x4e, 0x6d, 0x1a, 0xd5, 0xc8, 0xa9, 0x3d,
0x5c, 0xdd, 0x81, 0x00, 0x34, 0x9c, 0xe5, 0x95, 0x35, 0x99, 0x1e, 0x5e, 0xf3, 0xa2, 0xfd, 0xfb,
0x53, 0xdb, 0x0f, 0x31, 0x76, 0xb2, 0xcf, 0x22, 0x3c, 0x4a, 0x74, 0xff, 0xef, 0x35, 0x98, 0xd1,
0xb8, 0x67, 0xdf, 0x9d, 0xa2, 0xc1, 0x62, 0x23, 0xd0, 0xe4, 0x99, 0xc2, 0x67, 0x84, 0x97, 0x7b,
0x61, 0xca, 0x03, 0x91, 0x59, 0xdb, 0xe5, 0x6e, 0x1f, 0xfe, 0xf1, 0x14, 0x6c, 0x19, 0x44, 0x6a,
0x6f, 0x4e, 0x43, 0x14, 0x43, 0xa4, 0xa6, 0x9b, 0x31, 0x6d, 0x6f, 0x7f, 0x1d, 0x6c, 0xa0, 0xef,
0x83, 0x0d, 0xf4, 0x73, 0xb0, 0x81, 0x5e, 0x6e, 0x9d, 0xef, 0xa5, 0x70, 0x03, 0x01, 0xa1, 0x2e,
0x05, 0x0e, 0xaa, 0xe6, 0x61, 0xe8, 0xfc, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x3f, 0x7b, 0x15,
0xad, 0x06, 0x00, 0x00,
// 612 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x95, 0x4d, 0x6f, 0xd4, 0x3c,
0x10, 0xc7, 0xe5, 0x6d, 0x9f, 0x6d, 0xeb, 0x3e, 0x50, 0xb0, 0x0a, 0x8a, 0xd2, 0x17, 0x95, 0x80,
0xa0, 0xa0, 0xd6, 0x56, 0x5f, 0xb8, 0x70, 0xa3, 0x5b, 0x40, 0x2b, 0xf5, 0x42, 0x10, 0x17, 0x0e,
0x54, 0x6e, 0x32, 0xca, 0x9a, 0xa6, 0x89, 0xb1, 0x9d, 0x48, 0x05, 0x71, 0xe9, 0x89, 0x1b, 0x42,
0x5c, 0xb9, 0xf2, 0x41, 0xb8, 0x21, 0x71, 0x41, 0xe2, 0x0b, 0xa0, 0x15, 0x1f, 0x04, 0xc5, 0x49,
0x76, 0xd9, 0xad, 0xba, 0x2a, 0xd2, 0xc2, 0x69, 0x3d, 0xa3, 0x8c, 0xff, 0xbf, 0xf9, 0xdb, 0xb3,
0xc6, 0x8b, 0x1a, 0x54, 0x0e, 0x8a, 0x05, 0x71, 0xa6, 0x4d, 0xff, 0x97, 0x4a, 0x95, 0x9a, 0x94,
0x4c, 0x55, 0xa1, 0xbb, 0x18, 0xa5, 0x69, 0x14, 0x03, 0xe3, 0x52, 0x30, 0x9e, 0x24, 0xa9, 0xe1,
0x46, 0xa4, 0x89, 0x2e, 0x3f, 0x73, 0xf7, 0x22, 0x61, 0x3a, 0xd9, 0x01, 0x0d, 0xd2, 0x23, 0xc6,
0x55, 0x94, 0x4a, 0x95, 0xbe, 0xb0, 0x8b, 0xf5, 0x20, 0x64, 0xf9, 0x16, 0x93, 0x87, 0x51, 0x51,
0xa9, 0x19, 0x97, 0x32, 0x16, 0x81, 0xad, 0x65, 0xf9, 0x06, 0x8f, 0x65, 0x87, 0x6f, 0xb0, 0x08,
0x12, 0x50, 0xdc, 0x40, 0x58, 0xee, 0xe6, 0xdd, 0xc5, 0x33, 0xad, 0x52, 0xb6, 0xbd, 0x4b, 0x08,
0x9e, 0x34, 0xc7, 0x12, 0x1c, 0xb4, 0x82, 0x56, 0x67, 0x7c, 0xbb, 0x26, 0xf3, 0xf8, 0xbf, 0x9c,
0xc7, 0x19, 0x38, 0x0d, 0x9b, 0x2c, 0x03, 0xef, 0x15, 0xfe, 0xbf, 0x2a, 0x7b, 0x9c, 0x81, 0x3a,
0x26, 0x57, 0x71, 0xb3, 0xec, 0xad, 0xaa, 0xad, 0xa2, 0x62, 0xc7, 0x84, 0x1f, 0xd5, 0xc5, 0x76,
0x4d, 0x3c, 0xdc, 0x10, 0xa1, 0x33, 0xb1, 0x82, 0x56, 0x67, 0x37, 0x09, 0xad, 0x3d, 0xe8, 0x51,
0xf8, 0x0d, 0x11, 0x12, 0x17, 0x4f, 0x6b, 0x88, 0x21, 0x30, 0xa9, 0x72, 0x26, 0x6d, 0x6d, 0x2f,
0xf6, 0x2e, 0xe3, 0xb9, 0xea, 0x63, 0x1f, 0xb4, 0x4c, 0x13, 0x0d, 0xde, 0x3b, 0x84, 0xe7, 0xab,
0x5c, 0x4b, 0x01, 0x37, 0xe0, 0xc3, 0xcb, 0x0c, 0xb4, 0x21, 0xfb, 0xb8, 0x76, 0xd5, 0x82, 0xcd,
0x6e, 0x3e, 0xa0, 0x7d, 0xfb, 0x68, 0x6d, 0x9f, 0x5d, 0xec, 0x07, 0x21, 0xcd, 0xb7, 0xa8, 0x3c,
0x8c, 0x68, 0x61, 0x1f, 0xfd, 0xcd, 0x3e, 0x5a, 0xdb, 0x57, 0x53, 0xfa, 0xf5, 0xae, 0x45, 0xe3,
0x99, 0xd4, 0xa0, 0x8c, 0x6d, 0x71, 0xda, 0xaf, 0x22, 0xef, 0x73, 0x9f, 0xe8, 0xa9, 0x0c, 0xff,
0x25, 0xd1, 0x0d, 0x7c, 0x21, 0xb3, 0x8a, 0xe1, 0x43, 0x01, 0x71, 0xa8, 0x9d, 0xc6, 0xca, 0xc4,
0xea, 0x8c, 0x3f, 0x98, 0x3c, 0xcf, 0x21, 0x6c, 0x7e, 0x9d, 0xc2, 0x17, 0xab, 0xcc, 0x13, 0x50,
0xb9, 0x08, 0x80, 0x9c, 0x20, 0x3c, 0xb9, 0x27, 0xb4, 0x21, 0x57, 0x86, 0x6b, 0xec, 0x3d, 0x70,
0xdb, 0x63, 0x69, 0xa6, 0x50, 0xf0, 0x9c, 0x93, 0xef, 0x3f, 0x3f, 0x34, 0x08, 0xb9, 0x64, 0xe7,
0x20, 0xdf, 0xa8, 0xa7, 0x45, 0x93, 0xf7, 0x08, 0x37, 0xcb, 0x63, 0x26, 0x4b, 0xc3, 0x18, 0x03,
0xc7, 0xef, 0x8e, 0xc7, 0x5b, 0xef, 0x9a, 0x45, 0x59, 0xf0, 0x4e, 0xa1, 0xdc, 0xeb, 0xb9, 0xfe,
0x16, 0xe1, 0x89, 0x47, 0x70, 0xa6, 0x2f, 0x63, 0x02, 0xb9, 0x6e, 0x41, 0x96, 0xc8, 0xc2, 0x30,
0x08, 0x7b, 0x2d, 0x42, 0x6a, 0x47, 0xf3, 0x0d, 0xf9, 0x88, 0x70, 0xb3, 0xbc, 0x73, 0xa7, 0xed,
0x19, 0xb8, 0x8b, 0xe3, 0xa2, 0x5a, 0xb3, 0x54, 0x37, 0xdd, 0x51, 0x54, 0x7d, 0xa7, 0x9e, 0xe3,
0xe6, 0x2e, 0xc4, 0x60, 0xe0, 0x2c, 0xaf, 0x9c, 0xe1, 0x74, 0x6f, 0xcc, 0xab, 0xf6, 0xef, 0x8c,
0x6c, 0x3f, 0xc1, 0xd8, 0x2f, 0xfe, 0x32, 0xe1, 0x7e, 0x66, 0x3a, 0x7f, 0xae, 0xc1, 0xac, 0xc6,
0x6d, 0xef, 0xd6, 0x08, 0x0d, 0xa6, 0xac, 0xc0, 0x3a, 0x2f, 0x14, 0x3e, 0x21, 0x3c, 0xd7, 0x4e,
0x72, 0x1e, 0x8b, 0xc2, 0xda, 0x16, 0x0f, 0x3a, 0xf0, 0x97, 0x6f, 0xc1, 0xb6, 0x45, 0xa4, 0xde,
0xda, 0x28, 0x44, 0xd1, 0x43, 0x5a, 0x0f, 0x0a, 0xa6, 0x9d, 0x9d, 0x2f, 0xdd, 0x65, 0xf4, 0xad,
0xbb, 0x8c, 0x7e, 0x74, 0x97, 0xd1, 0xb3, 0xed, 0xf3, 0xbd, 0x22, 0x41, 0x2c, 0x20, 0x31, 0xb5,
0xc0, 0x41, 0xd3, 0x3e, 0x1a, 0x5b, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xc8, 0xfd, 0xc3,
0xc9, 0x06, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -738,6 +748,13 @@ func (m *ClusterQuery) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if len(m.Selector) > 0 {
i -= len(m.Selector)
copy(dAtA[i:], m.Selector)
i = encodeVarintCluster(dAtA, i, uint64(len(m.Selector)))
i--
dAtA[i] = 0x22
}
if m.Id != nil {
{
size, err := m.Id.MarshalToSizedBuffer(dAtA[:i])
@ -952,6 +969,10 @@ func (m *ClusterQuery) Size() (n int) {
l = m.Id.Size()
n += 1 + l + sovCluster(uint64(l))
}
l = len(m.Selector)
if l > 0 {
n += 1 + l + sovCluster(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -1265,6 +1286,38 @@ func (m *ClusterQuery) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Selector", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowCluster
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthCluster
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthCluster
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Selector = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipCluster(dAtA[iNdEx:])

View file

@ -11,6 +11,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes"
@ -69,6 +70,11 @@ func (s *Server) List(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Clus
// Filter clusters by server
filteredItems = filterClustersByServer(filteredItems, q.Server)
// Filter clusters by label selector
if filteredItems, err = filterClustersByLabels(filteredItems, q.Selector); err != nil {
return nil, fmt.Errorf("error filtering clusters by labels: %w", err)
}
items := make([]appv1.Cluster, 0)
for _, clust := range filteredItems {
if s.enf.Enforce(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionGet, CreateClusterRBACObject(clust.Project, clust.Server)) {
@ -140,6 +146,23 @@ func filterClustersByServer(clusters []appv1.Cluster, server string) []appv1.Clu
return items
}
func filterClustersByLabels(clusters []appv1.Cluster, selector string) ([]appv1.Cluster, error) {
if selector == "" {
return clusters, nil
}
ls, err := labels.Parse(selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector %q: %w", selector, err)
}
items := make([]appv1.Cluster, 0)
for i := range clusters {
if ls.Matches(labels.Set(clusters[i].Labels)) {
items = append(items, clusters[i])
}
}
return items, nil
}
// Create creates a cluster
func (s *Server) Create(ctx context.Context, q *cluster.ClusterCreateRequest) (*appv1.Cluster, error) {
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionCreate, CreateClusterRBACObject(q.Cluster.Project, q.Cluster.Server)); err != nil {

View file

@ -22,6 +22,8 @@ message ClusterQuery {
string server = 1;
string name = 2;
ClusterID id = 3;
// the selector to restrict returned list to clusters only with matched labels
string selector = 4;
}
message ClusterResponse {}

View file

@ -593,16 +593,19 @@ func TestListCluster(t *testing.T) {
Name: "foo",
Server: "https://127.0.0.1",
Namespaces: []string{"default", "kube-system"},
Labels: map[string]string{"env": "prod"},
}
barCluster := appv1.Cluster{
Name: "bar",
Server: "https://192.168.0.1",
Namespaces: []string{"default", "kube-system"},
Labels: map[string]string{"env": "staging"},
}
bazCluster := appv1.Cluster{
Name: "test/ing",
Server: "https://testing.com",
Namespaces: []string{"default", "kube-system"},
Labels: map[string]string{"env": "prod", "tier": "frontend"},
}
mockClusterList := appv1.ClusterList{
@ -679,6 +682,27 @@ func TestListCluster(t *testing.T) {
Items: []appv1.Cluster{barCluster},
},
},
{
name: "filter by label selector - single label",
q: &cluster.ClusterQuery{Selector: "env=prod"},
want: &appv1.ClusterList{
ListMeta: metav1.ListMeta{},
Items: []appv1.Cluster{fooCluster, bazCluster},
},
},
{
name: "filter by label selector - no match",
q: &cluster.ClusterQuery{Selector: "env=dev"},
want: &appv1.ClusterList{
ListMeta: metav1.ListMeta{},
Items: []appv1.Cluster{},
},
},
{
name: "filter by label selector - invalid selector",
q: &cluster.ClusterQuery{Selector: "!!invalid"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -695,6 +719,83 @@ func TestListCluster(t *testing.T) {
}
}
func TestFilterClustersByLabels(t *testing.T) {
t.Parallel()
prodFrontend := appv1.Cluster{
Name: "prod-frontend",
Server: "https://prod-frontend.example.com",
Labels: map[string]string{"env": "prod", "tier": "frontend"},
}
prodBackend := appv1.Cluster{
Name: "prod-backend",
Server: "https://prod-backend.example.com",
Labels: map[string]string{"env": "prod", "tier": "backend"},
}
stagingCluster := appv1.Cluster{
Name: "staging",
Server: "https://staging.example.com",
Labels: map[string]string{"env": "staging"},
}
noLabelsCluster := appv1.Cluster{
Name: "no-labels",
Server: "https://nolabels.example.com",
}
all := []appv1.Cluster{prodFrontend, prodBackend, stagingCluster, noLabelsCluster}
tests := []struct {
name string
selector string
want []appv1.Cluster
wantErr bool
}{
{
name: "empty selector returns all",
selector: "",
want: all,
},
{
name: "match by single label",
selector: "env=prod",
want: []appv1.Cluster{prodFrontend, prodBackend},
},
{
name: "match by multiple labels",
selector: "env=prod,tier=frontend",
want: []appv1.Cluster{prodFrontend},
},
{
name: "negation excludes matching clusters",
selector: "env!=staging",
want: []appv1.Cluster{prodFrontend, prodBackend, noLabelsCluster},
},
{
name: "no clusters match",
selector: "env=dev",
want: []appv1.Cluster{},
},
{
name: "invalid selector returns error",
selector: "!!invalid",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := filterClustersByLabels(all, tt.selector)
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestGetClusterAndVerifyAccess(t *testing.T) {
t.Run("GetClusterAndVerifyAccess - No Cluster", func(t *testing.T) {
db := &dbmocks.ArgoDB{}