Skip to content

Commit 52cbdba

Browse files
authored
🤖 feat: single-binary dual application (controller + aggregated API server) (#11)
## Summary Transforms the `coder-k8s` binary into a dual-application binary that supports two independent modes via `--app=controller|aggregated-apiserver`, running from the same container image. ## Background The project needs an aggregated API server alongside the existing controller to serve `CoderWorkspace` and `CoderTemplate` resources via `aggregation.coder.com/v1alpha1`. Both applications must run independently — the aggregated API server does not depend on controller-runtime. ## Implementation ### Mode-based dispatch - Refactored `main.go` into a thin entrypoint that delegates to a testable `run(args)` function in `app_dispatch.go` - `--app=controller` starts the existing controller-runtime manager (extracted into `internal/app/controllerapp/`) - `--app=aggregated-apiserver` starts the new aggregated API server (`internal/app/apiserverapp/`) - Missing or invalid `--app` values fail fast with assertion-style errors ### Aggregated API types - New API group `aggregation.coder.com/v1alpha1` with two resources: - `CoderWorkspace` — `spec.running bool`, `status.autoShutdown *metav1.Time` - `CoderTemplate` — same schema - Types registered via `k8s.io/apimachinery/pkg/runtime.SchemeBuilder` (not controller-runtime wrapper) - Deepcopy generated via updated `hack/update-codegen.sh` ### Aggregated API server - `GenericAPIServer` with self-signed TLS, anonymous auth, allow-all authz (scaffold defaults) - API group installation with hardcoded in-memory storage for both resources - Storage implements `rest.Storage`, `rest.Getter`, `rest.Lister`, `rest.Scoper`, `rest.SingularNameProvider` - Each resource returns 3 deterministic placeholder objects across `default` and `sandbox` namespaces - OpenAPI definitions provided; `SkipOpenAPIInstallation=true` for the scaffold phase ### Deployment manifests - `deploy/controller-deployment.yaml` — Deployment with `--app=controller` - `deploy/apiserver-deployment.yaml` — Deployment with `--app=aggregated-apiserver` - `deploy/apiserver-service.yaml` — Service for the aggregated API server - `deploy/apiserver-apiservice.yaml` — APIService registration for `v1alpha1.aggregation.coder.com` - `deploy/rbac.yaml` — ServiceAccount, auth-delegator ClusterRoleBinding, authentication-reader RoleBinding ### Tests - Mode dispatch: rejects empty, unknown, and stub modes - Controller scheme registration and health probe - Storage: List, Get, NotFound for both workspace and template - Aggregated server: scheme registration, API group installation + discovery smoke test ## Validation - `make test` ✅ — all tests pass - `make build` ✅ — binary compiles - `make verify-vendor` ✅ — vendor is in sync - `gofmt -l` ✅ — no formatting issues ## Risks - **Low:** The aggregated API server uses anonymous auth and allow-all authorization — appropriate for the scaffold phase but must be replaced with delegated auth before production use. - **Low:** OpenAPI installation is skipped (`SkipOpenAPIInstallation=true`); full OpenAPI serving requires generated definitions. --- <details> <summary>📋 Implementation Plan</summary> # Plan: Single-binary dual application (`controller` + aggregated API server) ## Context / Why We need one `coder-k8s` binary that can run **two distinct applications**: - the existing controller-runtime manager, and - a new aggregated API server serving `CoderWorkspace` and `CoderTemplate`. Per your direction, the aggregated API server must run independently from the controller-runtime instance. The cleanest shape is **mode-based dispatch in one binary** (one process runs one mode), with separate deployments using the same container image. ## Evidence - `main.go` currently always starts controller-runtime and has no mode dispatch. - `Dockerfile.goreleaser` has a single entrypoint (`/coder-k8s`), which supports argument-based mode selection without changing images. - `go.mod` does not include `k8s.io/apiserver` yet. - Existing API types include only `CoderControlPlane`; no aggregated API group/resources exist yet. - `hack/update-codegen.sh` currently generates deepcopy only for `api/v1alpha1`. ## Implementation details 1. **Refactor startup into explicit app modes in the existing root binary** - Keep one `main.go` binary and add a required/validated selector flag (e.g. `--app=controller|aggregated-apiserver`). - Dispatch to isolated run paths; unknown values fail fast with assertion-style errors. ```go switch appMode { case "controller": return runController(ctx) case "aggregated-apiserver": return runAggregatedAPIServer(ctx) default: return fmt.Errorf("assertion failed: unsupported --app %q", appMode) } ``` 2. **Move current controller-runtime wiring behind `runController`** - Extract today’s manager/reconciler setup from `main.go` into a dedicated package (e.g. `internal/app/controllerapp`). - Preserve behavior (scheme registration, health/readiness probes, reconciler setup) so `--app=controller` is functionally equivalent to current behavior. - Keep defensive assertions (`manager != nil`, `reconciler dependencies != nil`). 3. **Add aggregated API types for `CoderWorkspace` and `CoderTemplate`** - Create package `api/aggregation/v1alpha1` (group `aggregation.coder.com`, version `v1alpha1`). - Define resources with minimal scaffold fields: - `spec.running bool` - `status.autoShutdown *metav1.Time` - Register these types in a dedicated scheme builder and generate deepcopies. ```go type CoderTemplateSpec struct { Running bool `json:"running"` } type CoderTemplateStatus struct { AutoShutdown *metav1.Time `json:"autoShutdown,omitempty"` } ``` 4. **Implement aggregated API server app behind `runAggregatedAPIServer`** - Add dependencies: `k8s.io/apiserver` (+ any required companion libs like `k8s.io/component-base`). - Build a `GenericAPIServer` config with delegated authn/authz and secure serving. - Install one API group (`aggregation.coder.com/v1alpha1`) with two resources: - `coderworkspaces` - `codertemplates` - Keep this mode fully independent from controller-runtime manager startup. 5. **Add hardcoded storage implementations for scaffolding** - Add storage packages for both resources (e.g. `internal/aggregated/storage/...`). - Implement `rest.Storage` + `rest.Getter` + `rest.Lister` with deterministic hardcoded objects. - Return placeholder running states and fixed `autoShutdown` timestamps. - Add compile-time interface assertions and panic/return assertion failures for impossible conditions. 6. **Update codegen and wiring scripts** - Update `hack/update-codegen.sh` to include both API packages: - `./api/v1alpha1` - `./api/aggregation/v1alpha1` - Keep existing defensive checks for missing directories or empty `go list` results. 7. **Deploy as two independent apps from one image** - Add/adjust manifests so both workloads use the same image but different args: ```yaml # controller deployment args: ["--app=controller"] # aggregated apiserver deployment args: ["--app=aggregated-apiserver"] ``` - Add Service + APIService for aggregated mode (`v1alpha1.aggregation.coder.com`). - Include delegated-auth RBAC (`system:auth-delegator` and `extension-apiserver-authentication-reader`). 8. **Tests and validation** - Add tests for mode parsing/dispatch (controller mode, aggregated mode, invalid mode). - Add API scheme registration tests for new types. - Add storage tests for list/get + not found behavior. - Add aggregated server install smoke test (API group/resource registration). - Validate with `make codegen`, `make verify-vendor`, `make test`, `make build`. <details> <summary>Design choice: one process per mode (not both at once)</summary> This keeps operational boundaries clear and matches your requirement that the aggregated API server run independently from controller-runtime. If we later want an "all" mode, we can add it explicitly without changing this base architecture. </details> </details> --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh` • Cost: `$0.36`_ <!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh costs=0.36 -->
1 parent 4ad37e0 commit 52cbdba

2,272 files changed

Lines changed: 476139 additions & 5125 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Package v1alpha1 contains API schema definitions for the aggregation.coder.com API group.
2+
//
3+
// +k8s:deepcopy-gen=package
4+
// +groupName=aggregation.coder.com
5+
package v1alpha1
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package v1alpha1
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
"k8s.io/apimachinery/pkg/runtime"
6+
"k8s.io/apimachinery/pkg/runtime/schema"
7+
)
8+
9+
var (
10+
// SchemeGroupVersion is group version used to register these objects.
11+
SchemeGroupVersion = schema.GroupVersion{Group: "aggregation.coder.com", Version: "v1alpha1"}
12+
13+
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
14+
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
15+
16+
// AddToScheme adds the types in this group-version to the provided scheme.
17+
AddToScheme = SchemeBuilder.AddToScheme
18+
)
19+
20+
func addKnownTypes(scheme *runtime.Scheme) error {
21+
scheme.AddKnownTypes(SchemeGroupVersion,
22+
&CoderWorkspace{},
23+
&CoderWorkspaceList{},
24+
&CoderTemplate{},
25+
&CoderTemplateList{},
26+
)
27+
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
28+
return nil
29+
}
30+
31+
// Resource takes an unqualified resource and returns a Group-qualified GroupResource.
32+
func Resource(resource string) schema.GroupResource {
33+
return SchemeGroupVersion.WithResource(resource).GroupResource()
34+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package v1alpha1
2+
3+
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4+
5+
// CoderWorkspaceSpec defines the desired state of a CoderWorkspace.
6+
type CoderWorkspaceSpec struct {
7+
// Running indicates whether the workspace should be running.
8+
Running bool `json:"running"`
9+
}
10+
11+
// CoderWorkspaceStatus defines the observed state of a CoderWorkspace.
12+
type CoderWorkspaceStatus struct {
13+
// AutoShutdown is the next planned shutdown time for the workspace.
14+
AutoShutdown *metav1.Time `json:"autoShutdown,omitempty"`
15+
}
16+
17+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
18+
// +kubebuilder:object:root=true
19+
// +kubebuilder:subresource:status
20+
21+
// CoderWorkspace is the schema for Coder workspace resources.
22+
type CoderWorkspace struct {
23+
metav1.TypeMeta `json:",inline"`
24+
metav1.ObjectMeta `json:"metadata,omitempty"`
25+
26+
Spec CoderWorkspaceSpec `json:"spec,omitempty"`
27+
Status CoderWorkspaceStatus `json:"status,omitempty"`
28+
}
29+
30+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
31+
// +kubebuilder:object:root=true
32+
33+
// CoderWorkspaceList contains a list of CoderWorkspace objects.
34+
type CoderWorkspaceList struct {
35+
metav1.TypeMeta `json:",inline"`
36+
metav1.ListMeta `json:"metadata,omitempty"`
37+
Items []CoderWorkspace `json:"items"`
38+
}
39+
40+
// CoderTemplateSpec defines the desired state of a CoderTemplate.
41+
type CoderTemplateSpec struct {
42+
// Running indicates whether the template should be marked as running.
43+
Running bool `json:"running"`
44+
}
45+
46+
// CoderTemplateStatus defines the observed state of a CoderTemplate.
47+
type CoderTemplateStatus struct {
48+
// AutoShutdown is the next planned shutdown time for workspaces created by this template.
49+
AutoShutdown *metav1.Time `json:"autoShutdown,omitempty"`
50+
}
51+
52+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
53+
// +kubebuilder:object:root=true
54+
// +kubebuilder:subresource:status
55+
56+
// CoderTemplate is the schema for Coder template resources.
57+
type CoderTemplate struct {
58+
metav1.TypeMeta `json:",inline"`
59+
metav1.ObjectMeta `json:"metadata,omitempty"`
60+
61+
Spec CoderTemplateSpec `json:"spec,omitempty"`
62+
Status CoderTemplateStatus `json:"status,omitempty"`
63+
}
64+
65+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
66+
// +kubebuilder:object:root=true
67+
68+
// CoderTemplateList contains a list of CoderTemplate objects.
69+
type CoderTemplateList struct {
70+
metav1.TypeMeta `json:",inline"`
71+
metav1.ListMeta `json:"metadata,omitempty"`
72+
Items []CoderTemplate `json:"items"`
73+
}

‎api/aggregation/v1alpha1/zz_generated.deepcopy.go‎

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

‎app_dispatch.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
7+
ctrl "sigs.k8s.io/controller-runtime"
8+
9+
"github.com/coder/coder-k8s/internal/app/apiserverapp"
10+
"github.com/coder/coder-k8s/internal/app/controllerapp"
11+
)
12+
13+
var (
14+
runControllerApp = controllerapp.Run
15+
runAggregatedAPIServerApp = apiserverapp.Run
16+
)
17+
18+
func run(args []string) error {
19+
fs := flag.NewFlagSet("coder-k8s", flag.ContinueOnError)
20+
var appMode string
21+
fs.StringVar(&appMode, "app", "", "Application mode (controller, aggregated-apiserver)")
22+
if err := fs.Parse(args); err != nil {
23+
return err
24+
}
25+
26+
switch appMode {
27+
case "controller":
28+
return runControllerApp(ctrl.SetupSignalHandler())
29+
case "aggregated-apiserver":
30+
return runAggregatedAPIServerApp(ctrl.SetupSignalHandler())
31+
case "":
32+
return fmt.Errorf("assertion failed: --app flag is required; must be one of: controller, aggregated-apiserver")
33+
default:
34+
return fmt.Errorf("assertion failed: unsupported --app value %q; must be one of: controller, aggregated-apiserver", appMode)
35+
}
36+
}

‎config/e2e/deployment.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ spec:
1919
containers:
2020
- name: manager
2121
image: ghcr.io/coder/coder-k8s:e2e
22+
args: ["--app=controller"]
2223
imagePullPolicy: Never
2324
ports:
2425
- containerPort: 8081
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: apiregistration.k8s.io/v1
2+
kind: APIService
3+
metadata:
4+
name: v1alpha1.aggregation.coder.com
5+
spec:
6+
group: aggregation.coder.com
7+
version: v1alpha1
8+
service:
9+
name: coder-k8s-apiserver
10+
namespace: coder-system
11+
groupPriorityMinimum: 1000
12+
versionPriority: 100
13+
insecureSkipTLSVerify: true

0 commit comments

Comments
 (0)