Implement server-side filtering for VMs by worker (#392)

* Implement server-side filtering for VMs by worker

* Parse more than one filter but error out when more than one is provided

* Fix off-by-one

* No need to use "\n" in Debugf()
This commit is contained in:
Nikolay Edigaryev
2026-01-29 17:52:24 +01:00
committed by GitHub
parent 81a2c7b2df
commit 688238837a
15 changed files with 215 additions and 11 deletions
+25
View File
@@ -2,8 +2,11 @@ package client
import (
"crypto/x509"
"fmt"
"strings"
"github.com/cirruslabs/orchard/internal/dialer"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
)
type Option func(*Client)
@@ -32,3 +35,25 @@ func WithDialer(dialer dialer.Dialer) Option {
client.dialer = dialer
}
}
type ListInput struct {
Filters []v1.Filter
}
type ListOption func(params map[string]string)
func WithListFilters(filters ...v1.Filter) ListOption {
return func(params map[string]string) {
if len(filters) == 0 {
return
}
var pairs []string
for _, filter := range filters {
pairs = append(pairs, fmt.Sprintf("%s=%s", filter.Path, filter.Value))
}
params["filter"] = strings.Join(pairs, ",")
}
}
+15 -3
View File
@@ -57,13 +57,18 @@ func (service *VMsService) Create(ctx context.Context, vm *v1.VM) error {
}
func (service *VMsService) FindForWorker(ctx context.Context, worker string) ([]v1.VM, error) {
allVms, err := service.List(ctx)
allVms, err := service.List(ctx, WithListFilters(v1.Filter{
Path: "worker",
Value: worker,
}))
if err != nil {
return nil, err
}
var result []v1.VM
// Backwards compatibility with older Orchard Controllers
// that do not support the "filter" query parameter
for _, vmResource := range allVms {
if vmResource.Worker != worker {
continue
@@ -75,11 +80,18 @@ func (service *VMsService) FindForWorker(ctx context.Context, worker string) ([]
return result, nil
}
func (service *VMsService) List(ctx context.Context) ([]v1.VM, error) {
func (service *VMsService) List(ctx context.Context, opts ...ListOption) ([]v1.VM, error) {
params := map[string]string{}
// Apply options
for _, opt := range opts {
opt(params)
}
var vms []v1.VM
err := service.client.request(ctx, http.MethodGet, "vms",
nil, &vms, nil)
nil, &vms, params)
if err != nil {
return nil, err
}
+31
View File
@@ -0,0 +1,31 @@
package v1
import (
"errors"
"fmt"
"strings"
)
var ErrInvalidFilter = errors.New("invalid filter")
type Filter struct {
Path string
Value string
}
func NewFilter(s string) (Filter, error) {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
return Filter{}, fmt.Errorf("%w: expected path=value", ErrInvalidFilter)
}
if parts[0] == "" {
return Filter{}, fmt.Errorf("%w: path cannot be empty", ErrInvalidFilter)
}
return Filter{
Path: parts[0],
Value: parts[1],
}, nil
}
+55
View File
@@ -0,0 +1,55 @@
package v1_test
import (
"testing"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/stretchr/testify/require"
)
func TestNewFilter(t *testing.T) {
testCases := []struct {
Name string
Input string
Err error
Path string
Value string
}{
{
Name: "simple",
Input: "a.b.c=value",
Path: "a.b.c",
Value: "value",
},
{
Name: "value with equals",
Input: "a.b.c=d=e",
Path: "a.b.c",
Value: "d=e",
},
{
Name: "empty value",
Input: "a.b.c=",
Path: "a.b.c",
},
{
Name: "missing value",
Input: "abc",
Err: v1.ErrInvalidFilter,
},
{
Name: "missing path",
Input: "=value",
Err: v1.ErrInvalidFilter,
},
}
for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
filter, err := v1.NewFilter(testCase.Input)
require.ErrorIs(t, err, testCase.Err)
require.Equal(t, testCase.Path, filter.Path)
require.Equal(t, testCase.Value, filter.Value)
})
}
}
+4
View File
@@ -8,3 +8,7 @@ type ServiceAccount struct {
}
func (serviceAccount *ServiceAccount) SetVersion(_ uint64) {}
func (serviceAccount *ServiceAccount) Match(filter Filter) bool {
return false
}
+9
View File
@@ -102,6 +102,15 @@ func (vm *VM) SetVersion(version uint64) {
vm.Version = version
}
func (vm *VM) Match(filter Filter) bool {
switch filter.Path {
case "worker":
return vm.Worker == filter.Value
default:
return false
}
}
func (vm *VM) IsScheduled() bool {
if ConditionExists(vm.Conditions, ConditionTypeScheduled) {
return ConditionIsTrue(vm.Conditions, ConditionTypeScheduled)
+4
View File
@@ -32,3 +32,7 @@ func (worker Worker) Offline(workerOfflineTimeout time.Duration) bool {
}
func (worker *Worker) SetVersion(_ uint64) {}
func (worker *Worker) Match(filter Filter) bool {
return false
}