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
19 changes: 14 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,8 @@ jobs:
needs: [changes]
if: (needs.changes.outputs.images == 'true') || contains(fromJSON(needs.changes.outputs.ci-jobs || '[]'), 'build-images')
runs-on: ubuntu-latest
timeout-minutes: 15
# Include all image builds, cache exports, artifact upload and cleanup.
timeout-minutes: 30
permissions:
contents: read
steps:
Expand Down Expand Up @@ -1258,6 +1259,8 @@ jobs:
run: python3 charts/tests/check_gateway.py
- name: standalone panel isolation and persistence chart renders
run: python3 charts/tests/check_standalone.py
- name: auxiliary resources and management placement chart renders
run: python3 charts/tests/check_workloads.py
- name: operator Secret namespace permissions and cache configuration
run: python3 charts/tests/check_operator_secrets.py
- name: sanity-check rendered manifests
Expand Down Expand Up @@ -1821,13 +1824,17 @@ jobs:
# (operator does zero logins and is wait-dominated, so it runs
# wide; the api buckets stay within the login limiter's budget).
# The arm64 job below uses the same steps and per-bucket settings.
bucket: [operator, api-auth, api-roles, api-rbac, api-agent, api-mods, telemetry, optional-components]
bucket: [operator, api-auth, api-roles, api-rbac, api-agent, api-mods, telemetry, optional-components, management-placement]
arch: [amd64]
include:
- bucket: operator
parallel: 8
test_timeout: 20m
job_timeout: 60 # Boots a kind cluster, installs the chart, runs the bucket's suite.
- bucket: management-placement
parallel: 1
test_timeout: 15m
job_timeout: 60
- bucket: api-auth
parallel: 4
test_timeout: 15m
Expand Down Expand Up @@ -1884,7 +1891,9 @@ jobs:
arch: ${{ matrix.arch }}
- name: bring up cluster + helm install
if: env.RUN_REGEX != '' || env.TAIL_REGEX != ''
run: ./deploy/kind/e2e.sh up "$CLUSTER" e2e
env:
GAMEPLANE_E2E_PROFILE: ${{ matrix.bucket == 'management-placement' && 'management' || 'default' }}
run: ./deploy/kind/e2e.sh up "$CLUSTER" e2e "$GAMEPLANE_E2E_PROFILE"
- name: run go e2e (${{ matrix.bucket }})
if: env.RUN_REGEX != ''
run: |
Expand Down Expand Up @@ -2688,7 +2697,7 @@ jobs:
'go-e2e-unit': (n) => n === 'go (test/e2e)',
'api-postgres': (n) => n === 'api (postgres)',
'e2e-buckets': (n) => n === 'e2e bucket coverage',
'e2e-go': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components) \//.test(n) && n.includes('/ amd64 '),
'e2e-go': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|management-placement) \//.test(n) && n.includes('/ amd64 '),
'e2e-go-arm64': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components) \//.test(n) && n.includes('/ arm64 '),
'e2e-multicluster': (n) => n.startsWith('e2e multicluster /') && n.includes('/ amd64 '),
'e2e-multicluster-arm64': (n) => n.startsWith('e2e multicluster /') && n.includes('/ arm64 '),
Expand Down Expand Up @@ -2841,7 +2850,7 @@ jobs:
const bucketSet = new Set();
for (const j of apiJobs) {
const n = j.name || '';
const m = n.match(/^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components) \//);
const m = n.match(/^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|management-placement) \//);
if (m) bucketSet.add(m[1]);
if (/^e2e multicluster \//.test(n)) bucketSet.add('multicluster');
if (/^e2e upgrade \//.test(n)) bucketSet.add('upgrade');
Expand Down
6 changes: 6 additions & 0 deletions charts/gameplane/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
persistence and dashboard blocks are absent.
*/ -}}
{{- $r := .Values.api.telemetry.receiver }}
{{- if and (eq (include "gameplane.operatorEnabled" .) "true") (gt (int .Values.operator.replicas) 1) (not .Values.operator.leaderElect) }}

Operator leader election is enabled automatically because operator.replicas is
greater than one. The saved leaderElect=false value remains compatible with
upgrades; all operator replicas coordinate through the leader-election Lease.
{{- end }}
{{- if .Values.api.standalone }}

Standalone central panel: no local game cluster or Kubernetes API credentials.
Expand Down
23 changes: 23 additions & 0 deletions charts/gameplane/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,29 @@ true
{{- end -}}
{{- end -}}

{{- /* Shared PodSpec placement fields; absent keys preserve older reuse-values. */ -}}
{{- define "gameplane.placement" -}}
{{- range $key := list "nodeSelector" "tolerations" "affinity" "topologySpreadConstraints" "priorityClassName" -}}
{{- with index $ $key }}
{{ $key }}: {{ toYaml . | nindent 2 }}
{{- end -}}
{{- end -}}
{{- end -}}

{{- /* Complete auxiliary overrides; empty objects must not become defaults. */ -}}
{{- define "gameplane.auxiliaryResourceArgs" -}}
{{- range $name := list "agent" "sentinel" "tunnel" "backup" -}}
{{- $key := printf "%sResources" $name -}}
{{- $value := index $ $key -}}
{{- if and (hasKey $ $key) (not (kindIs "invalid" $value)) -}}
{{- if not (kindIs "map" $value) -}}
{{- fail (printf "operator.%s must be a resources object or null" $key) -}}
{{- end }}
- {{ printf "--%s-resources=%s" $name (toJson $value) | quote }}
{{- end -}}
{{- end -}}
{{- end -}}

{{- /* Validate every namespace before it can reach RBAC or cache flags. */}}
{{- define "gameplane.namespace" -}}
{{- if or (not (kindIs "string" .)) (gt (len (toString .)) 63) (not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" (toString .))) -}}
Expand Down
1 change: 1 addition & 0 deletions charts/gameplane/templates/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ spec:
checksum/mtls: {{ include "gameplane.mtlsChecksum" . }}
{{- end }}
spec:
{{- include "gameplane.placement" .Values.api | nindent 6 }}
serviceAccountName: gameplane-api
{{- if .Values.api.standalone }}
automountServiceAccountToken: false
Expand Down
1 change: 1 addition & 0 deletions charts/gameplane/templates/audit-syslog-bridge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ spec:
app.kubernetes.io/name: gameplane-audit-syslog-bridge
{{- include "gameplane.labels" . | nindent 8 }}
spec:
{{- include "gameplane.placement" $b | nindent 6 }}
securityContext:
runAsNonRoot: true
runAsUser: 65532
Expand Down
1 change: 1 addition & 0 deletions charts/gameplane/templates/crd-apply-hook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ spec:
app.kubernetes.io/name: gameplane-crd-apply
{{- include "gameplane.labels" . | nindent 8 }}
spec:
{{- include "gameplane.placement" .Values.crds.autoApply | nindent 6 }}
serviceAccountName: {{ .Release.Name }}-crd-apply
restartPolicy: Never
securityContext:
Expand Down
1 change: 1 addition & 0 deletions charts/gameplane/templates/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ spec:
app.kubernetes.io/name: gameplane-gateway
{{- include "gameplane.labels" . | nindent 8 }}
spec:
{{- include "gameplane.placement" $gateway | nindent 6 }}
serviceAccountName: gameplane-gateway
securityContext:
runAsNonRoot: true
Expand Down
1 change: 1 addition & 0 deletions charts/gameplane/templates/mcp-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ spec:
app.kubernetes.io/name: gameplane-mcp-server
{{- include "gameplane.labels" . | nindent 8 }}
spec:
{{- include "gameplane.placement" .Values.mcpServer | nindent 6 }}
serviceAccountName: gameplane-mcp-server
securityContext:
runAsNonRoot: true
Expand Down
85 changes: 85 additions & 0 deletions charts/gameplane/templates/operator-strategy-migrate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{{- /* SSA cannot remove a server-defaulted rollingUpdate field. Patch it before
Helm applies Recreate, including installations that manage CRDs externally. */ -}}
{{- if and .Release.IsUpgrade (eq (include "gameplane.operatorEnabled" .) "true") (not (or .Values.operator.leaderElect (gt (int .Values.operator.replicas) 1))) }}
{{- $existing := lookup "apps/v1" "Deployment" .Release.Namespace "gameplane-operator" }}
{{- if $existing }}
{{- $name := printf "%s-op-roll" .Release.Name }}
{{- $annotations := dict "helm.sh/hook" "pre-upgrade" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded" }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels: {{- include "gameplane.labels" . | nindent 4 }}
annotations: {{- toYaml (merge (dict "helm.sh/hook-weight" "-5") $annotations) | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels: {{- include "gameplane.labels" . | nindent 4 }}
annotations: {{- toYaml (merge (dict "helm.sh/hook-weight" "-5") $annotations) | nindent 4 }}
rules:
- apiGroups: [apps]
resources: [deployments]
resourceNames: [gameplane-operator]
verbs: [get, patch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels: {{- include "gameplane.labels" . | nindent 4 }}
annotations: {{- toYaml (merge (dict "helm.sh/hook-weight" "-5") $annotations) | nindent 4 }}
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: {{ $name }} }
subjects:
- kind: ServiceAccount
name: {{ $name }}
namespace: {{ .Release.Namespace }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels: {{- include "gameplane.labels" . | nindent 4 }}
annotations: {{- toYaml (merge (dict "helm.sh/hook-weight" "-4") $annotations) | nindent 4 }}
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 300
template:
spec:
{{- include "gameplane.placement" .Values.crds.autoApply | nindent 6 }}
serviceAccountName: {{ $name }}
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
seccompProfile: { type: RuntimeDefault }
containers:
- name: kubectl
image: {{ .Values.crds.autoApply.image | quote }}
imagePullPolicy: {{ .Values.crds.autoApply.pullPolicy }}
command: [kubectl, patch, deployment, gameplane-operator, --type=merge, -p, '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}']
env:
- { name: HOME, value: /tmp }
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 500m, memory: 256Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- name: tmp
emptyDir: {}
{{- with .Values.image.pullSecrets }}
imagePullSecrets: {{ toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
15 changes: 14 additions & 1 deletion charts/gameplane/templates/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,17 @@ metadata:
labels: {{- include "gameplane.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.operator.replicas }}
{{- if not (or .Values.operator.leaderElect (gt (int .Values.operator.replicas) 1)) }}
# Without leader election, prevent a rolling update from overlapping writers.
strategy:
type: Recreate
rollingUpdate: null
{{- else }}
# Explicitly restore rolling updates when election is re-enabled after a
# singleton install; the migration hook may own the previous Recreate field.
strategy:
type: RollingUpdate
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: gameplane-operator
Expand All @@ -280,6 +291,7 @@ spec:
annotations:
checksum/mtls: {{ include "gameplane.mtlsChecksum" . }}
spec:
{{- include "gameplane.placement" .Values.operator | nindent 6 }}
serviceAccountName: gameplane-operator
securityContext:
runAsNonRoot: true
Expand All @@ -291,7 +303,8 @@ spec:
image: {{ include "gameplane.operatorImage" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
{{- if .Values.operator.leaderElect }}
{{- include "gameplane.auxiliaryResourceArgs" .Values.operator | nindent 12 }}
{{- if or .Values.operator.leaderElect (gt (int .Values.operator.replicas) 1) }}
- --leader-elect
{{- end }}
- --agent-image={{ include "gameplane.agentImage" . }}
Expand Down
47 changes: 47 additions & 0 deletions charts/gameplane/templates/poddisruptionbudgets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{{- $apiEnabled := eq (include "gameplane.apiEnabled" .) "true" -}}
{{- $bridge := .Values.api.audit.webhook.syslogBridge -}}
{{- $receiver := .Values.api.telemetry.receiver -}}
{{- $gateway := .Values.gateway | default dict -}}
{{- $components := list
(dict "name" "operator" "config" .Values.operator "enabled" (eq (include "gameplane.operatorEnabled" .) "true"))
(dict "name" "api" "config" .Values.api "enabled" $apiEnabled)
(dict "name" "web" "config" .Values.web "enabled" (and $apiEnabled .Values.web.enabled))
(dict "name" "mcp-server" "config" .Values.mcpServer "enabled" .Values.mcpServer.enabled)
(dict "name" "gateway" "config" $gateway "enabled" $gateway.enabled)
(dict "name" "audit-syslog-bridge" "config" $bridge "enabled" (and $apiEnabled $bridge.enabled (not .Values.api.audit.webhook.url)))
(dict "name" "telemetry-receiver" "config" $receiver "enabled" (and $apiEnabled $receiver.enabled (not .Values.api.telemetry.endpoint))) -}}
{{- range $components -}}
{{- $pdb := .config.pdb | default dict -}}
{{- if and .enabled $pdb.enabled -}}
{{- $minimum := not (kindIs "invalid" $pdb.minAvailable) -}}
{{- $maximum := not (kindIs "invalid" $pdb.maxUnavailable) -}}
{{- if eq .name "api" -}}
{{- if or $minimum (and $maximum (ne (toString $pdb.maxUnavailable) "1")) -}}
{{- fail "api.pdb must allow the singleton API to be evicted: use maxUnavailable: 1 without minAvailable" -}}
{{- end -}}
{{- else if and $minimum $maximum -}}
{{- fail (printf "%s.pdb must set only one of minAvailable or maxUnavailable" .name) -}}
{{- end }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: gameplane-{{ .name }}
namespace: {{ $.Release.Namespace }}
labels: {{- include "gameplane.labels" $ | nindent 4 }}
spec:
{{- if eq .name "api" }}
maxUnavailable: 1
{{- else if $minimum }}
minAvailable: {{ toYaml $pdb.minAvailable }}
{{- else if $maximum }}
maxUnavailable: {{ toYaml $pdb.maxUnavailable }}
{{- else }}
maxUnavailable: 1
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: gameplane-{{ .name }}
app.kubernetes.io/instance: {{ $.Release.Name }}
{{- end -}}
{{- end -}}
1 change: 1 addition & 0 deletions charts/gameplane/templates/telemetry-receiver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ spec:
app.kubernetes.io/name: gameplane-telemetry-receiver
{{- include "gameplane.labels" . | nindent 8 }}
spec:
{{- include "gameplane.placement" $r | nindent 6 }}
securityContext:
runAsNonRoot: true
runAsUser: 65532
Expand Down
1 change: 1 addition & 0 deletions charts/gameplane/templates/web.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ spec:
app.kubernetes.io/name: gameplane-web
{{- include "gameplane.labels" . | nindent 8 }}
spec:
{{- include "gameplane.placement" .Values.web | nindent 6 }}
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
Expand Down
36 changes: 36 additions & 0 deletions charts/gameplane/values-production.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Example policy, not a capacity recommendation. Label/taint worker nodes before
# applying this profile; see docs/production-placement.md. Never remove or
# tolerate control-plane taints just to satisfy these selectors.
operator:
replicas: 2
leaderElect: true
nodeSelector: &managementNodes { gameplane.local/pool: management }
tolerations: &managementTolerations
- { key: gameplane.local/pool, operator: Equal, value: management, effect: NoSchedule }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app.kubernetes.io/name: gameplane-operator }
pdb: { enabled: true, maxUnavailable: 1 }
api:
replicas: 1
nodeSelector: *managementNodes
tolerations: *managementTolerations
pdb: { enabled: true, maxUnavailable: 1 }
web:
replicas: 2
nodeSelector: *managementNodes
tolerations: *managementTolerations
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app.kubernetes.io/name: gameplane-web }
pdb: { enabled: true, maxUnavailable: 1 }
crds:
autoApply:
nodeSelector: *managementNodes
tolerations: *managementTolerations
Loading
Loading