Skip to content
Open
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 @@ -439,7 +439,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 @@ -1232,6 +1233,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: sanity-check rendered manifests
run: |
grep -q 'kind: Deployment' /tmp/rendered.yaml
Expand Down Expand Up @@ -1793,13 +1796,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 @@ -1856,7 +1863,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 @@ -2660,7 +2669,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 @@ -2813,7 +2822,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
29 changes: 29 additions & 0 deletions agent/internal/rcon/nuclearoption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1275,18 +1275,47 @@ type pipeFrameCase struct {
forbidErr string // substring the error must NOT contain (e.g. a detail suffix)
}

type responseDeadlineConn struct {
net.Conn
deadlineSet chan<- struct{}
}

func (c *responseDeadlineConn) SetDeadline(deadline time.Time) error {
if err := c.Conn.SetDeadline(deadline); err != nil {
return err
}
select {
case c.deadlineSet <- struct{}{}:
default:
}
return nil
}

// runPipeFrameCases sends each case's raw frame back to the client over net.Pipe
// and checks the body or the error.
func runPipeFrameCases(t *testing.T, tests []pipeFrameCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deadlineSet := make(chan struct{}, 1)
client := newPipeClient(t, func(remote net.Conn) {
if _, _, err := readNuclearOptionRequest(remote); err != nil {
return
}
if len(tt.frame) == 0 {
// Exec sets its response deadline after sending the request.
// Closing first can fail SetDeadline instead of the status read.
select {
case <-deadlineSet:
case <-t.Context().Done():
return
}
}
writeFrameSegments(remote, tt.frame, tt.oneByte)
})
if len(tt.frame) == 0 {
client.conn = &responseDeadlineConn{Conn: client.conn, deadlineSet: deadlineSet}
}

body, err := client.Exec("test-command")
if tt.wantErr != "" {
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 @@ -135,3 +135,26 @@ true
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 -}}
1 change: 1 addition & 0 deletions charts/gameplane/templates/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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 .Values.operator.leaderElect) }}
{{- $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 }}
13 changes: 13 additions & 0 deletions charts/gameplane/templates/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,17 @@ metadata:
labels: {{- include "gameplane.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.operator.replicas }}
{{- if not .Values.operator.leaderElect }}
# 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 @@ -250,6 +261,7 @@ spec:
annotations:
checksum/mtls: {{ include "gameplane.mtlsChecksum" . }}
spec:
{{- include "gameplane.placement" .Values.operator | nindent 6 }}
serviceAccountName: gameplane-operator
securityContext:
runAsNonRoot: true
Expand All @@ -261,6 +273,7 @@ spec:
image: {{ include "gameplane.operatorImage" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
{{- include "gameplane.auxiliaryResourceArgs" .Values.operator | nindent 12 }}
{{- if .Values.operator.leaderElect }}
- --leader-elect
{{- end }}
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
3 changes: 3 additions & 0 deletions charts/gameplane/templates/validate.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{{- if and (eq (include "gameplane.operatorEnabled" .) "true") (gt (int .Values.operator.replicas) 1) (not .Values.operator.leaderElect) }}
{{- fail "operator.leaderElect must be enabled with more than one replica" }}
{{- end }}
{{- if .Values.api.standalone }}
{{- if ne (include "gameplane.apiEnabled" .) "true" }}
{{- fail "api.standalone requires api.enabled=true" }}
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
Loading
Loading