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
3 changes: 3 additions & 0 deletions api/v1alpha1/codercontrolplane_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ const (
// CoderControlPlaneSpec defines the desired state of a CoderControlPlane.
type CoderControlPlaneSpec struct {
// Image is the container image used for the Coder control plane pod.
// +kubebuilder:default="ghcr.io/coder/coder:latest"
Image string `json:"image,omitempty"`
// Replicas is the desired number of control plane pods.
// +kubebuilder:default=1
Replicas *int32 `json:"replicas,omitempty"`
// Service controls the service created in front of the control plane.
// +kubebuilder:default={}
Service ServiceSpec `json:"service,omitempty"`
// ExtraArgs are appended to the default Coder server arguments.
ExtraArgs []string `json:"extraArgs,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions api/v1alpha1/types_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ const (
// ServiceSpec defines the Service configuration reconciled by the operator.
type ServiceSpec struct {
// Type controls the Kubernetes service type.
// +kubebuilder:default="ClusterIP"
Type corev1.ServiceType `json:"type,omitempty"`
// Port controls the exposed service port.
// +kubebuilder:default=80
Comment thread
ThomasK33 marked this conversation as resolved.
Port int32 `json:"port,omitempty"`
// Annotations are applied to the reconciled service object.
Annotations map[string]string `json:"annotations,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions api/v1alpha1/workspaceproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type WorkspaceProxySpec struct {
// Replicas is the desired number of proxy pods.
Replicas *int32 `json:"replicas,omitempty"`
// Service controls the service created in front of the workspace proxy.
// +kubebuilder:default={}
Service ServiceSpec `json:"service,omitempty"`
// PrimaryAccessURL is the coderd URL the proxy should connect to.
PrimaryAccessURL string `json:"primaryAccessURL,omitempty"`
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/coder.com_codercontrolplanes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ spec:
type: object
type: array
image:
default: ghcr.io/coder/coder:latest
description: Image is the container image used for the Coder control
plane pod.
type: string
Expand All @@ -226,10 +227,12 @@ spec:
x-kubernetes-map-type: atomic
type: array
replicas:
default: 1
description: Replicas is the desired number of control plane pods.
format: int32
type: integer
service:
default: {}
description: Service controls the service created in front of the
control plane.
properties:
Expand All @@ -240,10 +243,12 @@ spec:
object.
type: object
port:
default: 80
description: Port controls the exposed service port.
format: int32
type: integer
type:
default: ClusterIP
description: Type controls the Kubernetes service type.
type: string
type: object
Expand Down
3 changes: 3 additions & 0 deletions config/crd/bases/coder.com_workspaceproxies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ spec:
format: int32
type: integer
service:
default: {}
description: Service controls the service created in front of the
workspace proxy.
properties:
Expand All @@ -303,10 +304,12 @@ spec:
object.
type: object
port:
default: 80
description: Port controls the exposed service port.
format: int32
type: integer
type:
default: ClusterIP
description: Type controls the Kubernetes service type.
type: string
type: object
Expand Down
19 changes: 19 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand Down Expand Up @@ -58,3 +65,15 @@ rules:
- get
- patch
- update
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
53 changes: 50 additions & 3 deletions internal/app/controllerapp/controllerapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ package controllerapp
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"time"

"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
Expand All @@ -19,6 +23,17 @@ import (
const (
// HealthProbeBindAddress exposes /healthz and /readyz checks for kube probes.
HealthProbeBindAddress = ":8081"

// leaderElectionID is the stable identity used for leader-election lease objects.
leaderElectionID = "coder-k8s-controller.coder.com"

// defaultLeaderElectionNamespace is used when the pod namespace cannot be
// detected (e.g. out-of-cluster development runs).
defaultLeaderElectionNamespace = "kube-system"

// inClusterNamespacePath is the standard path where Kubernetes injects the
// pod namespace when running inside a cluster.
inClusterNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
)

var setupLog = ctrl.Log.WithName("setup")
Expand All @@ -31,6 +46,9 @@ func NewScheme() *runtime.Scheme {
return scheme
}

// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// Run starts the controller-runtime manager for the controller application mode.
func Run(ctx context.Context) error {
if ctx == nil {
Expand All @@ -43,8 +61,12 @@ func Run(ctx context.Context) error {
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
HealthProbeBindAddress: HealthProbeBindAddress,
Scheme: scheme,
HealthProbeBindAddress: HealthProbeBindAddress,
LeaderElection: true,
LeaderElectionID: leaderElectionID,
LeaderElectionNamespace: detectLeaderElectionNamespace(),
LeaderElectionReleaseOnCancel: true,
Comment thread
ThomasK33 marked this conversation as resolved.
})
if err != nil {
return fmt.Errorf("unable to start manager: %w", err)
Expand Down Expand Up @@ -83,7 +105,14 @@ func Run(ctx context.Context) error {
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
return fmt.Errorf("unable to set up health check: %w", err)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
if err := mgr.AddReadyzCheck("readyz", func(req *http.Request) error {
ctx, cancel := context.WithTimeout(req.Context(), 2*time.Second)
defer cancel()
if synced := mgr.GetCache().WaitForCacheSync(ctx); !synced {
return fmt.Errorf("informer caches not synced")
}
return nil
}); err != nil {
return fmt.Errorf("unable to set up ready check: %w", err)
}

Expand All @@ -93,3 +122,21 @@ func Run(ctx context.Context) error {
}
return nil
}

// detectLeaderElectionNamespace returns the namespace to use for leader-election
// lease objects. Resolution order:
// 1. POD_NAMESPACE env var (allows explicit override for any environment).
// 2. In-cluster namespace file (standard Kubernetes downward API path).
// 3. defaultLeaderElectionNamespace as a last-resort fallback.
func detectLeaderElectionNamespace() string {
if ns := strings.TrimSpace(os.Getenv("POD_NAMESPACE")); ns != "" {
return ns
}
data, err := os.ReadFile(inClusterNamespacePath)
if err == nil {
if ns := strings.TrimSpace(string(data)); ns != "" {
return ns
}
}
return defaultLeaderElectionNamespace
Comment thread
ThomasK33 marked this conversation as resolved.
}
Loading