Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions internal/aggregated/coder/controlplane_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/url"
"sort"
"strings"
"time"

Expand All @@ -26,6 +27,7 @@ type ControlPlaneClientProvider struct {
var (
_ ClientProvider = (*ControlPlaneClientProvider)(nil)
_ NamespaceResolver = (*ControlPlaneClientProvider)(nil)
_ NamespaceLister = (*ControlPlaneClientProvider)(nil)
)

// NewControlPlaneClientProvider constructs a dynamic ClientProvider backed by CoderControlPlane resources.
Expand Down Expand Up @@ -206,6 +208,45 @@ func (p *ControlPlaneClientProvider) DefaultNamespace(ctx context.Context) (stri
}
}

// EligibleNamespaces returns namespaces served by eligible CoderControlPlane instances.
func (p *ControlPlaneClientProvider) EligibleNamespaces(ctx context.Context) ([]string, error) {
if p == nil {
return nil, fmt.Errorf("assertion failed: control plane client provider must not be nil")
}
if ctx == nil {
return nil, fmt.Errorf("assertion failed: context must not be nil")
}

eligible, err := p.findEligibleControlPlanes(ctx, "")
if err != nil {
return nil, err
}
if len(eligible) == 0 {
return nil, apierrors.NewServiceUnavailable(noEligibleControlPlaneMessage(""))
}

byNamespace := make(map[string]int, len(eligible))
for i := range eligible {
namespace := strings.TrimSpace(eligible[i].Namespace)
if namespace == "" {
return nil, fmt.Errorf("assertion failed: eligible CoderControlPlane namespace must not be empty")
}
byNamespace[namespace]++
}

namespaces := make([]string, 0, len(byNamespace))
for namespace, count := range byNamespace {
if count > 1 {
return nil, apierrors.NewBadRequest(multipleEligibleControlPlaneMessage(namespace))
}
namespaces = append(namespaces, namespace)
}

sort.Strings(namespaces)

return namespaces, nil
}

func (p *ControlPlaneClientProvider) findEligibleControlPlanes(
ctx context.Context,
namespace string,
Expand Down
155 changes: 155 additions & 0 deletions internal/aggregated/coder/controlplane_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,161 @@ func TestControlPlaneClientProviderDefaultNamespaceReturnsBadRequestForMultipleE
}
}

func TestControlPlaneClientProviderEligibleNamespacesMultipleNamespaces(t *testing.T) {
t.Parallel()

ignoredControlPlane := eligibleControlPlane("team-a", "coder-disabled")
ignoredControlPlane.Spec.OperatorAccess.Disabled = true

provider, secretReader := newControlPlaneProviderForTest(
t,
[]coderv1alpha1.CoderControlPlane{
eligibleControlPlane("team-a", "coder-a"),
eligibleControlPlane("team-b", "coder-b"),
ignoredControlPlane,
},
nil,
)

namespaces, err := provider.EligibleNamespaces(context.Background())
if err != nil {
t.Fatalf("resolve eligible namespaces: %v", err)
}
if got, want := secretReader.getCalls, 0; got != want {
t.Fatalf("expected %d secret reads, got %d", want, got)
}
if got, want := len(namespaces), 2; got != want {
t.Fatalf("expected %d namespaces, got %d: %v", want, got, namespaces)
}
if got, want := namespaces[0], "team-a"; got != want {
t.Fatalf("expected first namespace %q, got %q", want, got)
}
if got, want := namespaces[1], "team-b"; got != want {
t.Fatalf("expected second namespace %q, got %q", want, got)
}
}

func TestControlPlaneClientProviderEligibleNamespacesNoEligible(t *testing.T) {
t.Parallel()

provider, secretReader := newControlPlaneProviderForTest(t, nil, nil)

namespaces, err := provider.EligibleNamespaces(context.Background())
if err == nil {
t.Fatal("expected error")
}
if namespaces != nil {
t.Fatalf("expected nil namespaces on error, got %v", namespaces)
}
if got, want := secretReader.getCalls, 0; got != want {
t.Fatalf("expected %d secret reads, got %d", want, got)
}
if !apierrors.IsServiceUnavailable(err) {
t.Fatalf("expected ServiceUnavailable, got %v", err)
}
if !strings.Contains(err.Error(), "no eligible CoderControlPlane") {
t.Fatalf("expected no-eligible message, got %v", err)
}
}

func TestControlPlaneClientProviderEligibleNamespacesDuplicateInNamespace(t *testing.T) {
t.Parallel()

provider, secretReader := newControlPlaneProviderForTest(
t,
[]coderv1alpha1.CoderControlPlane{
eligibleControlPlane("team-a", "coder-a"),
eligibleControlPlane("team-a", "coder-b"),
},
nil,
)

namespaces, err := provider.EligibleNamespaces(context.Background())
if err == nil {
t.Fatal("expected error")
}
if namespaces != nil {
t.Fatalf("expected nil namespaces on error, got %v", namespaces)
}
if got, want := secretReader.getCalls, 0; got != want {
t.Fatalf("expected %d secret reads, got %d", want, got)
}
if !apierrors.IsBadRequest(err) {
t.Fatalf("expected BadRequest, got %v", err)
}
if !strings.Contains(err.Error(), "multi-instance support is planned") {
t.Fatalf("expected multi-instance message, got %v", err)
}
}

func TestControlPlaneClientProviderEligibleNamespacesSingleNamespace(t *testing.T) {
t.Parallel()

provider, secretReader := newControlPlaneProviderForTest(
t,
[]coderv1alpha1.CoderControlPlane{eligibleControlPlane("team-a", "coder-a")},
nil,
)

namespaces, err := provider.EligibleNamespaces(context.Background())
if err != nil {
t.Fatalf("resolve eligible namespaces: %v", err)
}
if got, want := secretReader.getCalls, 0; got != want {
t.Fatalf("expected %d secret reads, got %d", want, got)
}
if got, want := len(namespaces), 1; got != want {
t.Fatalf("expected %d namespace, got %d", want, got)
}
if got, want := namespaces[0], "team-a"; got != want {
t.Fatalf("expected namespace %q, got %q", want, got)
}
}

func TestControlPlaneClientProviderEligibleNamespacesAssertions(t *testing.T) {
t.Parallel()

provider, _ := newControlPlaneProviderForTest(t, nil, nil)

tests := []struct {
name string
provider *ControlPlaneClientProvider
ctx context.Context
wantErrContains string
}{
{
name: "rejects nil provider",
provider: nil,
ctx: context.Background(),
wantErrContains: "assertion failed: control plane client provider must not be nil",
},
{
name: "rejects nil context",
provider: provider,
ctx: nil,
wantErrContains: "assertion failed: context must not be nil",
},
}

for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

namespaces, err := testCase.provider.EligibleNamespaces(testCase.ctx)
if err == nil {
t.Fatalf("expected error containing %q, got nil", testCase.wantErrContains)
}
if namespaces != nil {
t.Fatalf("expected nil namespaces on error, got %v", namespaces)
}
if !strings.Contains(err.Error(), testCase.wantErrContains) {
t.Fatalf("expected error containing %q, got %q", testCase.wantErrContains, err.Error())
}
})
}
}

func TestControlPlaneClientProviderClientForNamespaceDefaultsSecretKeyToToken(t *testing.T) {
t.Parallel()

Expand Down
19 changes: 19 additions & 0 deletions internal/aggregated/coder/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ type NamespaceResolver interface {
DefaultNamespace(ctx context.Context) (string, error)
}

// NamespaceLister can enumerate namespaces served by a ClientProvider.
// Used to implement all-namespaces LIST by fanning out across instances.
type NamespaceLister interface {
EligibleNamespaces(ctx context.Context) ([]string, error)
}

// StaticClientProvider returns one static client, optionally restricted to one namespace.
type StaticClientProvider struct {
Client *codersdk.Client
Expand All @@ -31,6 +37,7 @@ type StaticClientProvider struct {
var (
_ ClientProvider = (*StaticClientProvider)(nil)
_ NamespaceResolver = (*StaticClientProvider)(nil)
_ NamespaceLister = (*StaticClientProvider)(nil)
)

// ClientForNamespace returns the static client.
Expand Down Expand Up @@ -77,6 +84,18 @@ func (p *StaticClientProvider) DefaultNamespace(_ context.Context) (string, erro
return p.Namespace, nil
}

// EligibleNamespaces returns namespaces served by the static provider.
func (p *StaticClientProvider) EligibleNamespaces(_ context.Context) ([]string, error) {
if p == nil {
return nil, fmt.Errorf("assertion failed: static client provider must not be nil")
}
if p.Namespace == "" {
return nil, apierrors.NewServiceUnavailable("static provider has no default namespace")
}

return []string{p.Namespace}, nil
}

// NewStaticClientProvider creates a StaticClientProvider from cfg and optional namespace restriction.
func NewStaticClientProvider(cfg Config, namespace string) (*StaticClientProvider, error) {
client, err := NewSDKClient(cfg)
Expand Down
52 changes: 52 additions & 0 deletions internal/aggregated/coder/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,58 @@ func TestStaticClientProviderDefaultNamespaceAssertions(t *testing.T) {
}
}

func TestStaticClientProviderEligibleNamespaces(t *testing.T) {
t.Parallel()

provider := &StaticClientProvider{Namespace: "control-plane"}
namespaces, err := provider.EligibleNamespaces(context.Background())
if err != nil {
t.Fatalf("resolve eligible namespaces: %v", err)
}
if got, want := len(namespaces), 1; got != want {
t.Fatalf("expected %d namespace, got %d", want, got)
}
if got, want := namespaces[0], "control-plane"; got != want {
t.Fatalf("expected namespace %q, got %q", want, got)
}
}

func TestStaticClientProviderEligibleNamespacesNoNamespace(t *testing.T) {
t.Parallel()

provider := &StaticClientProvider{}
namespaces, err := provider.EligibleNamespaces(context.Background())
if err == nil {
t.Fatal("expected error")
}
if namespaces != nil {
t.Fatalf("expected nil namespaces on error, got %v", namespaces)
}
if !apierrors.IsServiceUnavailable(err) {
t.Fatalf("expected ServiceUnavailable, got %v", err)
}
if !strings.Contains(err.Error(), "static provider has no default namespace") {
t.Fatalf("expected missing namespace message, got %v", err)
}
}

func TestStaticClientProviderEligibleNamespacesNilReceiver(t *testing.T) {
t.Parallel()

var provider *StaticClientProvider
namespaces, err := provider.EligibleNamespaces(context.Background())
if err == nil {
t.Fatal("expected error")
}
if namespaces != nil {
t.Fatalf("expected nil namespaces on error, got %v", namespaces)
}
wantErrContains := "assertion failed: static client provider must not be nil"
if !strings.Contains(err.Error(), wantErrContains) {
t.Fatalf("expected error containing %q, got %q", wantErrContains, err.Error())
}
}

func TestNewStaticClientProvider(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 2 additions & 0 deletions internal/aggregated/storage/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
// while Kubernetes object names allow dots (DNS-1123 subdomains).
// - A single admin session token is used for all API calls (no per-request impersonation in v1).
// - Storage resolves the backing codersdk.Client via a ClientProvider interface.
// - All-namespaces LIST aggregates results across eligible CoderControlPlane namespaces
// when the provider implements NamespaceLister.
package storage
Loading
Loading