Commit 52cbdba
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
- api/aggregation/v1alpha1
- config/e2e
- deploy
- hack
- internal
- aggregated/storage
- app
- apiserverapp
- controllerapp
- vendor
- cel.dev/expr
- github.com
- NYTimes/gziphandler
- antlr4-go/antlr/v4
- blang/semver/v4
- cenkalti/backoff/v4
- coreos
- go-semver
- semver
- go-systemd/v22
- daemon
- journal
- felixge/httpsnoop
- go-logr
- logr/funcr
- stdr
- gogo/protobuf
- gogoproto
- protoc-gen-gogo/descriptor
- proto
- golang/protobuf
- proto
- google
- cel-go
- cel
- templates
- checker
- decls
- common
- ast
- containers
- debug
- decls
- env
- functions
- operators
- overloads
- runes
- stdlib
- types
- pb
- ref
- traits
- ext
- interpreter
- functions
- parser
- gen
- go-cmp/cmp
- internal
- diff
- flags
- function
- value
- grpc-ecosystem
- go-grpc-prometheus
- grpc-gateway/v2
- internal/httprule
- protoc-gen-openapiv2/options
- runtime
- utilities
- kylelemons/godebug
- diff
- prometheus/client_golang/prometheus/testutil
- promlint
- validations
- stoewer/go-strcase
- go.etcd.io/etcd
- api/v3
- authpb
- etcdserverpb
- membershippb
- mvccpb
- v3rpc/rpctypes
- versionpb
- version
- client
- pkg/v3
- fileutil
- logutil
- systemd
- tlsutil
- transport
- types
- verify
- v3
- credentials
- internal
- endpoint
- resolver
- kubernetes
- go.opentelemetry.io
- auto/sdk
- internal/telemetry
- contrib/instrumentation
- google.golang.org/grpc/otelgrpc
- internal
- net/http/otelhttp
- internal
- request
- semconvutil
- semconv
- otel
- attribute
- internal
- baggage
- codes
- exporters/otlp/otlptrace
- internal/tracetransform
- otlptracegrpc
- internal
- envconfig
- otlpconfig
- retry
- internal
- baggage
- global
- metric
- embedded
- noop
- propagation
- sdk
- instrumentation
- internal
- env
- x
- resource
- trace
- semconv
- internal
- v1.12.0
- v1.17.0
- v1.20.0
- v1.26.0
- trace
- embedded
- internal/telemetry
- noop
- proto/otlp
- collector/trace/v1
- common/v1
- resource/v1
- trace/v1
- go.uber.org/zap/zapgrpc
- golang.org/x
- crypto
- cryptobyte
- asn1
- hkdf
- internal
- alias
- poly1305
- nacl/secretbox
- salsa20/salsa
- exp
- constraints
- slices
- net
- context
- internal/timeseries
- trace
- websocket
- sync/singleflight
- sys
- cpu
- windows/registry
- text
- feature/plural
- internal
- catmsg
- format
- number
- stringset
- message
- catalog
- google.golang.org
- genproto/googleapis
- api
- annotations
- expr/v1alpha1
- httpbody
- rpc
- errdetails
- status
- grpc
- attributes
- backoff
- balancer
- base
- endpointsharding
- grpclb/state
- pickfirst
- internal
- pickfirstleaf
- roundrobin
- binarylog/grpc_binarylog_v1
- channelz
- codes
- connectivity
- credentials
- insecure
- encoding
- gzip
- proto
- experimental/stats
- grpclog
- internal
- health/grpc_health_v1
- internal
- backoff
- balancerload
- balancer/gracefulswitch
- binarylog
- buffer
- channelz
- credentials
- envconfig
- grpclog
- grpcsync
- grpcutil
- idle
- metadata
- pretty
- proxyattributes
- resolver
- delegatingresolver
- dns
- internal
- passthrough
- unix
- serviceconfig
- stats
- status
- syscall
- transport
- networktype
- keepalive
- mem
- metadata
- peer
- resolver
- dns
- manual
- serviceconfig
- stats
- status
- tap
- protobuf
- encoding/protojson
- internal
- editionssupport
- encoding/json
- protoadapt
- reflect/protodesc
- types
- dynamicpb
- gofeaturespb
- known
- durationpb
- emptypb
- fieldmaskpb
- structpb
- wrapperspb
- gopkg.in/natefinch/lumberjack.v2
- k8s.io
- apimachinery
- pkg
- apis
- asn1
- meta
- v1beta1/validation
- v1
- api
- validate
- content
- validation/path
- runtime/serializer/cbor/internal/modes
- util
- diff
- httpstream
- wsstream
- intstr
- net
- portforward
- rand
- remotecommand
- sort
- strategicpatch
- validation/field
- waitgroup
- third_party/forked/golang/reflect
- apiserver
- pkg
- admission
- configuration
- initializer
- metrics
- plugin
- authorizer
- cel
- namespace/lifecycle
- policy
- generic
- internal/generic
- matching
- mutating
- metrics
- patch
- validating
- metrics
- webhook
- config
- apis/webhookadmission
- v1alpha1
- v1
- errors
- generic
- matchconditions
- mutating
- predicates
- namespace
- object
- rules
- request
- validating
- apis
- apidiscovery/v2
- apiserver
- install
- v1alpha1
- v1beta1
- v1
- validation
- audit
- install
- v1
- validation
- cel
- flowcontrol/bootstrap
- audit
- policy
- authentication
- authenticatorfactory
- authenticator
- cel
- group
- request
- anonymous
- bearertoken
- headerrequest
- union
- websocket
- x509
- serviceaccount
- token
- cache
- tokenfile
- user
- authorization
- authorizerfactory
- authorizer
- cel
- path
- union
- cel
- common
- environment
- lazy
- library
- mutation
- dynamic
- openapi
- resolver
- endpoints
- deprecation
- discovery
- aggregated
- filterlatency
- filters
- impersonation
- handlers
- fieldmanager
- finisher
- metrics
- negotiation
- responsewriters
- metrics
- openapi
- request
- responsewriter
- warning
- features
- quota/v1
- registry
- generic
- registry
- rest
- server
- dynamiccertificates
- egressselector
- metrics
- filters
- flagz
- api/v1alpha1
- negotiate
- healthz
- httplog
- mux
- options
- encryptionconfig
- controller
- metrics
- resourceconfig
- routes
- routine
- statusz
- api/v1alpha1
- negotiate
- storage
- storageversion
- storage
- cacher
- delegator
- metrics
- progress
- errors
- etcd3
- metrics
- feature
- names
- storagebackend
- factory
- value
- encrypt
- aes
- envelope
- kmsv2
- v2
- metrics
- identity
- secretbox
- util
- apihelpers
- compatibility
- configmetrics
- dryrun
- feature
- flowcontrol
- debug
- fairqueuing
- eventclock
- promise
- queueset
- format
- metrics
- request
- flushwriter
- peerproxy/metrics
- shufflesharding
- webhook
- x509metrics
- validation
- warning
- plugin/pkg
- audit
- buffered
- log
- truncate
- webhook
- authenticator/token/webhook
- authorizer/webhook
- metrics
- api/imagepolicy/v1alpha1
- client-go
- applyconfigurations
- imagepolicy/v1alpha1
- discovery
- cached/memory
- fake
- dynamic
- dynamicinformer
- dynamiclister
- fake
- kubernetes
- fake
- typed
- admissionregistration
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- apiserverinternal/v1alpha1/fake
- apps
- v1beta1/fake
- v1beta2/fake
- v1/fake
- authentication
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- authorization
- v1beta1/fake
- v1/fake
- autoscaling
- v1/fake
- v2beta1/fake
- v2beta2/fake
- v2/fake
- batch
- v1beta1/fake
- v1/fake
- certificates
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- coordination
- v1alpha2/fake
- v1beta1/fake
- v1/fake
- core/v1/fake
- discovery
- v1beta1/fake
- v1/fake
- events
- v1beta1/fake
- v1/fake
- extensions/v1beta1/fake
- flowcontrol
- v1beta1/fake
- v1beta2/fake
- v1beta3/fake
- v1/fake
- networking
- v1beta1/fake
- v1/fake
- node
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- policy
- v1beta1/fake
- v1/fake
- rbac
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- resource
- v1alpha3/fake
- v1beta1/fake
- v1beta2/fake
- v1/fake
- scheduling
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- storagemigration/v1beta1/fake
- storage
- v1alpha1/fake
- v1beta1/fake
- v1/fake
- openapi/cached
- rest/fake
- component-base
- cli/flag
- compatibility
- featuregate
- logs
- api/v1
- internal/setverbositylevel
- klogflags
- metrics
- features
- legacyregistry
- prometheusextension
- prometheus
- compatversion
- feature
- slis
- workqueue
- testutil
- tracing
- api/v1
- version
- zpages/features
- klog/v2
- internal/verbosity
- textlogger
- kms
- apis
- v1beta1
- v2
- pkg
- service
- util
- kube-openapi/pkg
- builder3
- util
- builder
- common/restfuladapter
- handler
- internal/third_party/govalidator
- schemamutation
- validation
- errors
- strfmt
- bson
- utils/path
- sigs.k8s.io/apiserver-network-proxy/konnectivity-client
- pkg
- client
- metrics
- common/metrics
- proto/client
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
0 commit comments