atlas-iac/services/jenkins/configmap-jcasc.yaml
jenkins 11063daaaa feat(hermes-triage-demo): isolated demo surface + Jenkins job + Hermes key seeding
- New hermes-triage-demo namespace with fixture PVC and RBAC scoped to
  Jenkins agent Job creation only
- JCasC pipelineJob hermes-triage-demo: SEED_FAILURE-parameterized fixture
  check running as a Kubernetes Job in the demo namespace, emitting the
  incident ID to pod stdout (Fluent Bit -> OpenSearch kube-*) and JUnit to
  Jenkins
- Hermes init container can seed API_SERVER_KEY in the persistent .env
  from an optional hermes-api-server-key Secret (no-op until it exists)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 16:42:37 -03:00

752 lines
26 KiB
YAML

# services/jenkins/configmap-jcasc.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: jenkins-jcasc
namespace: jenkins
data:
securityrealm.yaml: |
jenkins:
securityRealm:
oic:
clientId: "${OIDC_CLIENT_ID}"
clientSecret: "${OIDC_CLIENT_SECRET}"
serverConfiguration:
wellKnown:
wellKnownOpenIDConfigurationUrl: "${OIDC_ISSUER}/.well-known/openid-configuration"
scopesOverride: "openid profile email"
logoutFromOpenIdProvider: true
postLogoutRedirectUrl: "https://ci.bstein.dev"
sendScopesInTokenRequest: true
rootURLFromRequest: false
userNameField: "preferred_username"
fullNameFieldName: "name"
emailFieldName: "email"
groupsFieldName: "groups"
authorization.yaml: |
jenkins:
authorizationStrategy:
loggedInUsersCanDoAnything:
allowAnonymousRead: false
creds.yaml: |
credentials:
system:
domainCredentials:
- credentials:
- usernamePassword:
scope: GLOBAL
id: gitea-pat
username: "${GITEA_PAT_USERNAME}"
password: "${GITEA_PAT_TOKEN}"
description: "Gitea PAT for pipelines"
- usernamePassword:
scope: GLOBAL
id: harbor-robot
username: "${HARBOR_ROBOT_USERNAME}"
password: "${HARBOR_ROBOT_PASSWORD}"
description: "Harbor robot for pipelines"
- usernamePassword:
scope: GLOBAL
id: harbor-robot-streaming
username: "${HARBOR_STREAMING_ROBOT_USERNAME}"
password: "${HARBOR_STREAMING_ROBOT_PASSWORD}"
description: "Harbor robot for streaming pushes"
- string:
scope: GLOBAL
id: sonarqube-token
secret: "${SONARQUBE_TOKEN}"
description: "SonarQube token for quality-gate evidence collection"
jobs.yaml: |
jobs:
- script: |
pipelineJob('harbor-arm-build') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/harbor-arm-build.git')
credentials('gitea-pat')
}
branches('*/master')
}
}
}
}
}
pipelineJob('bstein-dev-home') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/2 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/bstein-dev-home.git')
credentials('gitea-pat')
}
branches('*/master')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('ariadne') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/2 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/ariadne.git')
credentials('gitea-pat')
}
branches('*/master')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('metis') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/metis.git')
credentials('gitea-pat')
}
branches('*/master')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('ananke') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/ananke.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('lesavka') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/lesavka.git')
credentials('gitea-pat')
}
branches('*/master')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('arcanagon') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/arcanagon.git')
credentials('gitea-pat')
}
branches('*/master')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('pegasus') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/pegasus.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('atlasbot') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/atlasbot.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('soteria') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/soteria.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('data-prepper') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/titan-iac.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('services/logging/Jenkinsfile.data-prepper')
}
}
}
pipelineJob('titan-iac') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H/12 * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/titan-iac.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('typhon') {
properties {
pipelineTriggers {
triggers {
scmTrigger {
scmpoll_spec('H/5 * * * *')
ignorePostCommitHooks(false)
}
cron {
spec('H H * * *')
}
}
}
}
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/typhon.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('cassandra') {
disabled(false)
description('Staged Cassandra alpha image pipeline. Backend/frontend should build linux/amd64 and linux/arm64; sim-worker may begin amd64-only if Forge dependencies require it.')
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/bstein/cassandra.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
}
pipelineJob('hermes-triage-demo') {
disabled(false)
description('Hermes automated-triage demo: runs a deterministic fixture check as a Kubernetes Job in the hermes-triage-demo namespace. Build with SEED_FAILURE=true to arm the demo failure; the automated repair loop rebuilds with SEED_FAILURE=false.')
parameters {
booleanParam('SEED_FAILURE', false, 'Write "unhealthy" into the demo fixture before the check (arms the demo failure).')
}
definition {
cps {
sandbox(true)
script('''
podTemplate(
cloud: 'kubernetes',
yaml: """
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins
securityContext:
runAsUser: 1001
runAsGroup: 1001
nodeSelector:
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
containers:
- name: kubectl
image: bitnami/kubectl@sha256:554ab88b1858e8424c55de37ad417b16f2a0e65d1607aa0f3fe3ce9b9f10b131
command: ["sleep"]
args: ["3600"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
"""
) {
node(POD_LABEL) {
container('kubectl') {
stage('Fixture check') {
def buildNum = env.BUILD_NUMBER
def incidentId = "hermes-triage-demo/${buildNum}"
def jobName = "hermes-demo-test-${buildNum}"
def seed = params.SEED_FAILURE ? "true" : "false"
writeFile file: 'test-runner-job.yaml', text: """
apiVersion: batch/v1
kind: Job
metadata:
name: ${jobName}
namespace: hermes-triage-demo
labels:
app.kubernetes.io/part-of: hermes-triage-demo
atlas.bstein.dev/role: demo-test-runner
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/part-of: hermes-triage-demo
atlas.bstein.dev/role: demo-test-runner
spec:
restartPolicy: Never
containers:
- name: test-runner
image: busybox:1.37
env:
- name: INCIDENT_ID
value: "${incidentId}"
- name: SEED_FAILURE
value: "${seed}"
command:
- sh
- -c
- |
STATE_FILE=/fixture/state
if [ "\\$SEED_FAILURE" = "true" ]; then
echo unhealthy > "\\$STATE_FILE"
fi
if [ ! -f "\\$STATE_FILE" ]; then
echo healthy > "\\$STATE_FILE"
fi
STATE=\\$(cat "\\$STATE_FILE")
if [ "\\$STATE" = "healthy" ]; then
echo "{\\\\"event\\\\":\\\\"hermes_demo_test_pass\\\\",\\\\"incident_id\\\\":\\\\"\\$INCIDENT_ID\\\\",\\\\"message\\\\":\\\\"fixture state healthy\\\\"}"
exit 0
fi
echo "{\\\\"event\\\\":\\\\"hermes_demo_test_failure\\\\",\\\\"incident_id\\\\":\\\\"\\$INCIDENT_ID\\\\",\\\\"classification_hint\\\\":\\\\"demo_fixture_unhealthy\\\\",\\\\"message\\\\":\\\\"expected fixture state healthy; found \\$STATE\\\\"}"
exit 1
volumeMounts:
- name: fixture
mountPath: /fixture
resources:
requests:
cpu: 25m
memory: 16Mi
limits:
cpu: 100m
memory: 32Mi
volumes:
- name: fixture
persistentVolumeClaim:
claimName: hermes-triage-demo-fixture
"""
sh "kubectl -n hermes-triage-demo create -f test-runner-job.yaml"
def verdict = "unknown"
timeout(time: 3, unit: 'MINUTES') {
waitUntil(initialRecurrencePeriod: 5000) {
def st = sh(script: "kubectl -n hermes-triage-demo get job ${jobName} -o jsonpath='{.status.succeeded}:{.status.failed}'", returnStdout: true).trim()
if (st.startsWith("1")) { verdict = "pass"; return true }
if (st.endsWith(":1")) { verdict = "fail"; return true }
return false
}
}
def runnerLog = sh(script: "kubectl -n hermes-triage-demo logs job/${jobName} --tail=20 || true", returnStdout: true).trim()
echo "test-runner output:\\n${runnerLog}"
def junitBody
if (verdict == "pass") {
junitBody = """<testsuite name="hermes-triage-demo" tests="1" failures="0"><testcase classname="hermes-triage-demo" name="fixture-state-check"/></testsuite>"""
} else {
def safeLog = runnerLog.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
junitBody = """<testsuite name="hermes-triage-demo" tests="1" failures="1"><testcase classname="hermes-triage-demo" name="fixture-state-check"><failure message="demo fixture unhealthy">${safeLog}</failure></testcase></testsuite>"""
}
writeFile file: 'demo-junit.xml', text: junitBody
junit 'demo-junit.xml'
if (verdict != "pass") {
error("Demo fixture check failed for incident ${incidentId}")
}
}
}
}
}
''')
}
}
}
multibranchPipelineJob('titan-iac-quality-gate') {
branchSources {
branchSource {
source {
git {
id('titan-iac-quality-gate')
remote('https://scm.bstein.dev/bstein/titan-iac.git')
credentialsId('gitea-pat')
}
}
}
}
factory {
workflowBranchProjectFactory {
scriptPath('ci/Jenkinsfile.titan-iac')
}
}
orphanedItemStrategy {
discardOldItems {
numToKeep(30)
}
}
triggers {
periodicFolderTrigger {
interval('12h')
}
}
configure { node ->
def webhookToken = System.getenv('TITAN_IAC_WEBHOOK_TOKEN') ?: ''
def triggers = node / 'triggers'
def webhook = triggers.appendNode('com.igalg.jenkins.plugins.mswt.trigger.ComputedFolderWebHookTrigger')
webhook.appendNode('token', webhookToken)
}
}
base.yaml: |
jenkins:
disableRememberMe: false
mode: NORMAL
numExecutors: 0
labelString: ""
projectNamingStrategy: "standard"
markupFormatter:
plainText
globalNodeProperties:
- envVars:
env:
- key: "GIT_CONFIG_COUNT"
value: "1"
- key: "GIT_CONFIG_KEY_0"
value: "safe.directory"
- key: "GIT_CONFIG_VALUE_0"
value: "*"
clouds:
- kubernetes:
containerCapStr: "5"
connectTimeout: "20"
readTimeout: "90"
jenkinsUrl: "http://jenkins.jenkins.svc.cluster.local:8080"
jenkinsTunnel: "jenkins.jenkins.svc.cluster.local:50000"
skipTlsVerify: false
maxRequestsPerHostStr: "32"
retentionTimeout: "5"
waitForPodSec: "600"
name: "kubernetes"
namespace: "jenkins"
restrictedPssSecurityContext: false
serverUrl: "https://kubernetes.default"
credentialsId: ""
podLabels:
- key: "jenkins/jenkins-jenkins-agent"
value: "true"
templates:
- name: "default"
namespace: "jenkins"
workspaceVolume:
dynamicPVC:
accessModes: "ReadWriteOnce"
requestsSize: "20Gi"
storageClassName: "astreae"
containers:
- name: "jnlp"
args: "^${computer.jnlpmac} ^${computer.name}"
envVars:
- envVar:
key: "JENKINS_URL"
value: "http://jenkins.jenkins.svc.cluster.local:8080/"
image: "jenkins/inbound-agent:3355.v388858a_47b_33-3"
privileged: "false"
resourceLimitCpu: 512m
resourceLimitMemory: 512Mi
resourceRequestCpu: 512m
resourceRequestMemory: 512Mi
ttyEnabled: false
workingDir: /home/jenkins/agent
idleMinutes: 0
instanceCap: 2147483647
label: "jenkins-jenkins-agent "
nodeUsageMode: "NORMAL"
podRetention: Never
serviceAccount: "jenkins"
slaveConnectTimeoutStr: "100"
yaml: |
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
nodeSelector:
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- titan-06
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: atlas.bstein.dev/spillover
operator: DoesNotExist
- weight: 95
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- titan-13
- titan-15
- titan-17
- titan-19
- weight: 85
preference:
matchExpressions:
- key: hardware
operator: In
values:
- rpi5
- weight: 45
preference:
matchExpressions:
- key: hardware
operator: In
values:
- rpi4
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
jenkins/jenkins-jenkins-agent: "true"
yamlMergeStrategy: override
inheritYamlMergeStrategy: false
slaveAgentPort: 50000
crumbIssuer:
standard:
excludeClientIPFromCrumb: true
unclassified:
location:
url: "https://ci.bstein.dev/"