feat(ql3): admit plugin secret action jobs

This commit is contained in:
whyour
2026-08-14 10:22:14 +08:00
parent 75dbe7a4c4
commit 7f5339e047
27 changed files with 2019 additions and 10 deletions
@@ -0,0 +1,12 @@
# Example only. Copy into a private overlay and replace the placeholder with
# the exact Kubernetes API endpoint CIDR. The base policy intentionally has no
# broad TCP/443 escape hatch, so the controller fails closed without this rule.
- op: add
path: /spec/egress/-
value:
to:
- ipBlock:
cidr: REPLACE_WITH_API_SERVER_CIDR
ports:
- protocol: TCP
port: 443
@@ -26,7 +26,7 @@ spec:
app.kubernetes.io/part-of: qinglong3
spec:
serviceAccountName: ql3-plugin-package-executor
automountServiceAccountToken: false
automountServiceAccountToken: true
restartPolicy: Never
securityContext:
runAsNonRoot: true
@@ -65,6 +65,82 @@ spec:
value: '16'
- name: QL3_PLUGIN_PACKAGE_EXECUTOR_REVOCATION_MAX_PAGES
value: '16'
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED
value: 'true'
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_LIMIT
value: '8'
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_NAMESPACE
value: qinglong3-system
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_SERVICE_ACCOUNT
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: serviceAccountName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_SOURCE_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: sourceSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: image
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresCaSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresCaKey
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_SERVERNAME
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresServerName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresUrlSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresUrlSecretKey
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_AUTH_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresAuthSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_HOST
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresHost
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PORT
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresPort
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_DATABASE
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresDatabase
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_USERNAME_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresUsernameKey
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PASSWORD_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresPasswordKey
- name: QL3_POSTGRES_TLS_MODE
value: verify-full
- name: QL3_POSTGRES_TLS_CA_FILE
@@ -3,5 +3,11 @@ kind: Kustomization
resources:
- service-account.yaml
- secret-action-service-account.yaml
- secret-action-admission-config.yaml
- role.yaml
- role-binding.yaml
- validating-admission-policy.yaml
- validating-admission-policy-binding.yaml
- cron-job.yaml
- network-policy.yaml
@@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ql3-plugin-package-executor
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-plugin-package-executor
app.kubernetes.io/component: plugin-package-executor
app.kubernetes.io/part-of: qinglong3
subjects:
- kind: ServiceAccount
name: ql3-plugin-package-executor
namespace: qinglong3-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ql3-plugin-package-executor
@@ -0,0 +1,25 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ql3-plugin-package-executor
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-plugin-package-executor
app.kubernetes.io/component: plugin-package-executor
app.kubernetes.io/part-of: qinglong3
rules:
- apiGroups:
- batch
resources:
- jobs
verbs:
- create
- get
- apiGroups:
- ''
resources:
- configmaps
resourceNames:
- ql3-plugin-package-secret-action-admission
verbs:
- get
@@ -0,0 +1,27 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ql3-plugin-package-secret-action-admission
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-plugin-package-secret-action
app.kubernetes.io/component: plugin-package-executor
app.kubernetes.io/part-of: qinglong3
data:
# Release automation must replace this unavailable digest together with the
# controller image before the opt-in operation can execute.
image: registry.example.com/qinglong/qinglong3-cluster-admin@sha256:0000000000000000000000000000000000000000000000000000000000000000
serviceAccountName: ql3-plugin-package-secret-action
sourceSecretName: ql3-cluster-plugin-package-values
postgresConnectionMode: url
postgresCaSecretName: ql3-cluster-plugin-package-executor
postgresCaKey: postgres-ca.crt
postgresServerName: REPLACE_WITH_POSTGRES_DNS_NAME
postgresUrlSecretName: ql3-cluster-plugin-package-executor
postgresUrlSecretKey: postgres-package-executor-url
postgresAuthSecretName: ''
postgresHost: ''
postgresPort: ''
postgresDatabase: ''
postgresUsernameKey: ''
postgresPasswordKey: ''
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: ql3-plugin-package-secret-action
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-plugin-package-secret-action
app.kubernetes.io/component: plugin-package-executor
app.kubernetes.io/part-of: qinglong3
automountServiceAccountToken: false
@@ -0,0 +1,20 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: ql3-plugin-package-secret-action
labels:
app.kubernetes.io/name: ql3-plugin-package-secret-action
app.kubernetes.io/component: plugin-package-executor
app.kubernetes.io/part-of: qinglong3
spec:
policyName: ql3-plugin-package-secret-action
paramRef:
name: ql3-plugin-package-secret-action-admission
namespace: qinglong3-system
parameterNotFoundAction: Deny
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: qinglong3-system
validationActions:
- Deny
@@ -0,0 +1,163 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: ql3-plugin-package-secret-action
labels:
app.kubernetes.io/name: ql3-plugin-package-secret-action
app.kubernetes.io/component: plugin-package-executor
app.kubernetes.io/part-of: qinglong3
spec:
failurePolicy: Fail
paramKind:
apiVersion: v1
kind: ConfigMap
matchConstraints:
resourceRules:
- apiGroups:
- batch
apiVersions:
- v1
operations:
- CREATE
resources:
- jobs
scope: Namespaced
matchConditions:
- name: exact-controller-identity
expression: request.userInfo.username == 'system:serviceaccount:qinglong3-system:ql3-plugin-package-executor'
variables:
- name: pod
expression: object.spec.template.spec
- name: executor
expression: variables.pod.containers[0]
- name: values
expression: variables.pod.volumes.filter(v, v.name == 'plugin-package-values')[0]
- name: ca
expression: variables.pod.volumes.filter(v, v.name == 'postgres-ca')[0]
validations:
- expression: >-
object.metadata.namespace == 'qinglong3-system' &&
object.metadata.name.matches('^ql3-package-secret-[0-9a-f]{32}$') &&
object.metadata.name == 'ql3-package-secret-' + object.metadata.annotations['qinglong.io/secret-action-job-digest'].substring(0, 32) &&
object.metadata.labels['app.kubernetes.io/name'] == 'ql3-plugin-package-secret-action' &&
object.metadata.labels['app.kubernetes.io/component'] == 'plugin-package-executor' &&
object.metadata.labels['app.kubernetes.io/part-of'] == 'qinglong3' &&
object.metadata.annotations['qinglong.io/secret-action-job-schema'] == 'qinglong/plugin-package-kubernetes-secret-action-job@v1' &&
object.metadata.annotations['qinglong.io/secret-action-job-digest'].matches('^[0-9a-f]{64}$') &&
object.metadata.annotations['qinglong.io/approved-action-digest'].matches('^[0-9a-f]{64}$') &&
object.metadata.annotations['qinglong.io/approved-action-type'] in ['plugin_package.secret_binding.bind', 'plugin_package.secret_binding.transition']
message: Secret action Job metadata is outside the reviewed contract
- expression: >-
object.spec.backoffLimit == 2 &&
object.spec.activeDeadlineSeconds == 600 &&
object.spec.ttlSecondsAfterFinished == 3600 &&
variables.pod.serviceAccountName == params.data.serviceAccountName &&
object.spec.template.metadata.labels['app.kubernetes.io/name'] == 'ql3-plugin-package-secret-action' &&
object.spec.template.metadata.labels['app.kubernetes.io/component'] == 'plugin-package-executor' &&
object.spec.template.metadata.labels['app.kubernetes.io/part-of'] == 'qinglong3' &&
object.spec.template.metadata.labels['qinglong.io/secret-action-job'] == object.metadata.name &&
variables.pod.automountServiceAccountToken == false &&
variables.pod.enableServiceLinks == false &&
variables.pod.restartPolicy == 'Never' &&
(!has(variables.pod.hostNetwork) || variables.pod.hostNetwork == false) &&
(!has(variables.pod.hostPID) || variables.pod.hostPID == false) &&
(!has(variables.pod.hostIPC) || variables.pod.hostIPC == false) &&
(!has(variables.pod.shareProcessNamespace) || variables.pod.shareProcessNamespace == false) &&
(!has(variables.pod.initContainers) || variables.pod.initContainers.size() == 0) &&
(!has(variables.pod.ephemeralContainers) || variables.pod.ephemeralContainers.size() == 0) &&
variables.pod.containers.size() == 1 &&
variables.pod.volumes.size() == 3
message: Secret action Job Pod authority is outside the reviewed contract
- expression: >-
variables.pod.securityContext.runAsNonRoot == true &&
variables.pod.securityContext.runAsUser == 10001 &&
variables.pod.securityContext.runAsGroup == 10001 &&
variables.pod.securityContext.fsGroup == 10001 &&
variables.pod.securityContext.seccompProfile.type == 'RuntimeDefault' &&
variables.executor.name == 'executor' &&
variables.executor.image == params.data.image &&
variables.executor.imagePullPolicy == 'IfNotPresent' &&
variables.executor.command == ['node', '/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/plugin-package/executor/pluginPackageExecutorCli.js'] &&
variables.executor.securityContext.allowPrivilegeEscalation == false &&
variables.executor.securityContext.readOnlyRootFilesystem == true &&
variables.executor.securityContext.capabilities.drop == ['ALL'] &&
variables.executor.resources.requests.cpu == '25m' &&
variables.executor.resources.requests.memory == '48Mi' &&
variables.executor.resources.limits.cpu == '250m' &&
variables.executor.resources.limits.memory == '192Mi'
message: Secret action Job executor image, command or sandbox is outside the reviewed contract
- expression: >-
((params.data.postgresConnectionMode == 'url' && variables.executor.env.size() == 11) ||
(params.data.postgresConnectionMode == 'fields' && variables.executor.env.size() == 15)) &&
variables.executor.env.all(e, e.name in [
'QL3_PLUGIN_PACKAGE_EXECUTOR_ENABLED',
'QL3_PLUGIN_PACKAGE_EXECUTOR_OWNER',
'QL3_PLUGIN_PACKAGE_EXECUTOR_DISPATCH_ID',
'QL3_PLUGIN_PACKAGE_EXECUTOR_SECRET_ROOT',
'QL3_PLUGIN_PACKAGE_EXECUTOR_LEASE_DURATION_MS',
'QL3_POSTGRES_TLS_MODE',
'QL3_POSTGRES_TLS_CA_FILE',
'QL3_POSTGRES_TLS_SERVERNAME',
'QL3_POSTGRES_APPLICATION_NAME',
'QL3_POSTGRES_MAX_CONNECTIONS',
'QL3_POSTGRES_PACKAGE_EXECUTOR_URL',
'QL3_POSTGRES_PACKAGE_EXECUTOR_HOST',
'QL3_POSTGRES_PACKAGE_EXECUTOR_PORT',
'QL3_POSTGRES_PACKAGE_EXECUTOR_DATABASE',
'QL3_POSTGRES_PACKAGE_EXECUTOR_USER',
'QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD'
]) &&
variables.executor.env.exists_one(e, e.name == 'QL3_PLUGIN_PACKAGE_EXECUTOR_ENABLED' && e.value == 'true') &&
variables.executor.env.exists_one(e, e.name == 'QL3_PLUGIN_PACKAGE_EXECUTOR_OWNER' && e.value.matches('^package_secret_[0-9a-f]{24}$')) &&
variables.executor.env.exists_one(e, e.name == 'QL3_PLUGIN_PACKAGE_EXECUTOR_DISPATCH_ID' && e.value.matches('^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$')) &&
variables.executor.env.exists_one(e, e.name == 'QL3_PLUGIN_PACKAGE_EXECUTOR_SECRET_ROOT' && e.value == '/var/run/secrets/qinglong3/plugin-package-values') &&
variables.executor.env.exists_one(e, e.name == 'QL3_PLUGIN_PACKAGE_EXECUTOR_LEASE_DURATION_MS' && e.value == '600000') &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_TLS_MODE' && e.value == 'verify-full') &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_TLS_CA_FILE' && e.value == '/var/run/secrets/qinglong3/postgres/ca.crt') &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_TLS_SERVERNAME' && e.value == params.data.postgresServerName) &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_APPLICATION_NAME' && e.value == 'qinglong3-package-secret-action') &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_MAX_CONNECTIONS' && e.value == '1')
message: Secret action Job environment is outside the reviewed contract
- expression: >-
(params.data.postgresConnectionMode == 'url' &&
variables.executor.env.exists_one(e,
e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_URL' &&
e.valueFrom.secretKeyRef.name == params.data.postgresUrlSecretName &&
e.valueFrom.secretKeyRef.key == params.data.postgresUrlSecretKey) &&
!variables.executor.env.exists(e, e.name in ['QL3_POSTGRES_PACKAGE_EXECUTOR_HOST', 'QL3_POSTGRES_PACKAGE_EXECUTOR_PORT', 'QL3_POSTGRES_PACKAGE_EXECUTOR_DATABASE', 'QL3_POSTGRES_PACKAGE_EXECUTOR_USER', 'QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD'])) ||
(params.data.postgresConnectionMode == 'fields' &&
!variables.executor.env.exists(e, e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_URL') &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_HOST' && e.value == params.data.postgresHost) &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_PORT' && e.value == params.data.postgresPort) &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_DATABASE' && e.value == params.data.postgresDatabase) &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_USER' && e.valueFrom.secretKeyRef.name == params.data.postgresAuthSecretName && e.valueFrom.secretKeyRef.key == params.data.postgresUsernameKey) &&
variables.executor.env.exists_one(e, e.name == 'QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD' && e.valueFrom.secretKeyRef.name == params.data.postgresAuthSecretName && e.valueFrom.secretKeyRef.key == params.data.postgresPasswordKey))
message: Secret action Job database authority is outside the reviewed contract
- expression: >-
variables.executor.volumeMounts.size() == 3 &&
variables.executor.volumeMounts[0].name == 'tmp' &&
variables.executor.volumeMounts[0].mountPath == '/tmp' &&
(!has(variables.executor.volumeMounts[0].readOnly) || variables.executor.volumeMounts[0].readOnly == false) &&
variables.executor.volumeMounts[1].name == 'postgres-ca' &&
variables.executor.volumeMounts[1].mountPath == '/var/run/secrets/qinglong3/postgres' &&
variables.executor.volumeMounts[1].readOnly == true &&
variables.executor.volumeMounts[2].name == 'plugin-package-values' &&
variables.executor.volumeMounts[2].mountPath == '/var/run/secrets/qinglong3/plugin-package-values' &&
variables.executor.volumeMounts[2].readOnly == true &&
variables.pod.volumes.exists_one(v, v.name == 'tmp' && v.emptyDir.medium == 'Memory' && v.emptyDir.sizeLimit == '8Mi') &&
variables.ca.secret.secretName == params.data.postgresCaSecretName &&
variables.ca.secret.optional == false &&
variables.ca.secret.defaultMode == 292 &&
variables.ca.secret.items.size() == 1 &&
variables.ca.secret.items[0].key == params.data.postgresCaKey &&
variables.ca.secret.items[0].path == 'ca.crt' &&
((has(variables.values.secret) &&
variables.values.secret.secretName == params.data.sourceSecretName &&
variables.values.secret.optional == false &&
variables.values.secret.defaultMode == 288 &&
variables.values.secret.items.size() >= 1 &&
variables.values.secret.items.size() <= 64 &&
variables.values.secret.items.all(i, i.key.matches('^[0-9a-f]{64}$') && i.path == i.key) &&
variables.values.secret.items.all(i, variables.values.secret.items.filter(j, j.key == i.key).size() == 1)) ||
(has(variables.values.emptyDir) && variables.values.emptyDir.sizeLimit == '1Ki'))
message: Secret action Job volumes are outside the exact-key projection contract
@@ -17,6 +17,82 @@
value: '16'
- name: QL3_PLUGIN_PACKAGE_EXECUTOR_REVOCATION_MAX_PAGES
value: '16'
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED
value: 'true'
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_LIMIT
value: '8'
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_NAMESPACE
value: qinglong3-system
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_SERVICE_ACCOUNT
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: serviceAccountName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_SOURCE_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: sourceSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: image
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresCaSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresCaKey
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_SERVERNAME
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresServerName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresUrlSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresUrlSecretKey
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_AUTH_SECRET
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresAuthSecretName
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_HOST
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresHost
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PORT
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresPort
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_DATABASE
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresDatabase
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_USERNAME_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresUsernameKey
- name: QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PASSWORD_KEY
valueFrom:
configMapKeyRef:
name: ql3-plugin-package-secret-action-admission
key: postgresPasswordKey
- name: QL3_POSTGRES_TLS_MODE
value: verify-full
- name: QL3_POSTGRES_TLS_CA_FILE
@@ -17,6 +17,11 @@ patches:
kind: NetworkPolicy
name: ql3-plugin-package-executor
path: network-policy-patch.yaml
- target:
version: v1
kind: ConfigMap
name: ql3-plugin-package-secret-action-admission
path: secret-action-admission-config-patch.yaml
images:
- name: qinglong3-cluster-admin
@@ -0,0 +1,18 @@
- op: replace
path: /data
value:
image: registry.example.com/qinglong/qinglong3-cluster-admin@sha256:0000000000000000000000000000000000000000000000000000000000000000
serviceAccountName: ql3-plugin-package-secret-action
sourceSecretName: ql3-cluster-plugin-package-values
postgresConnectionMode: fields
postgresCaSecretName: ql3-postgres-ca
postgresCaKey: ca.crt
postgresServerName: ql3-postgres-rw.qinglong3-system.svc
postgresUrlSecretName: ''
postgresUrlSecretKey: ''
postgresAuthSecretName: ql3-postgres-package-executor-auth
postgresHost: ql3-postgres-rw.qinglong3-system.svc
postgresPort: '5432'
postgresDatabase: qinglong
postgresUsernameKey: username
postgresPasswordKey: password
@@ -21,3 +21,7 @@ metadata:
namespace: qinglong3-system
type: Opaque
data: {}
# The controller also needs one private-overlay NetworkPolicy patch containing
# only the exact Kubernetes API endpoint CIDR and TCP port. Start from
# api-server-egress-patch.example.yaml; do not permit arbitrary TCP/443 egress.
File diff suppressed because one or more lines are too long
@@ -42,3 +42,8 @@ D-306B1 只允许给当前 active 且尚未绑定的 Package generation 做首
- Cluster management/executor 的 separation-of-duty 产品编排仍未消费 transition plan;真实 Kubernetes rotation/revoke、升级失败回滚及物理低配设备证据也仍待完成。因此 ADR 继续保持 Proposed,下一切片把既有 package-manager Approval 与 package-executor 接入这条 durable authority,再进入 Kubernetes/低配现场门。
- 后续切片已经完成 Cluster transition Approval/执行、Kubernetes active pointer v3 与三节点 rotation/revoke rollout 原语现场门;该历史未完成描述由本条取代。当前 executor 重构进一步增加 exact `dispatchById` start-barrier 路径和由 durable dispatch + approval plan 联合绑定的 action-scoped Job renderer。常规 batch CronJob 不再挂载 `ql3-cluster-plugin-package-values`,也不注册需要 Secret projection 的 handleraction Job 只投影单动作所需的去重 SHA-256 key,固定 `0440``optional:false`、digest 镜像、tokenless ServiceAccount、单连接 PostgreSQL 与 48 MiB request。零 Secret transition 使用空目录而不是空 Secret `items`。因此“整个 optional Secret 暴露给批处理 executor”的生产缺口已经失败关闭,但自动 controller 仍未启用:Kubernetes `jobs.create` 是可间接选择镜像/ServiceAccount/Secret 的放大权限,必须由 ValidatingAdmissionPolicy(或同等级外部 admission)把 renderer 契约固定后,才允许 create/get-only RBAC、响应丢失收敛与失败 Job 恢复。升级失败自动回滚、controller/admission/RBAC 真实门及固定物理低配证据继续阻断 ADR Accepted。
- 本阶段完整性门已闭合:18-package clean build/test 退出 0backend 1195 项为 1193 pass、2 条条件 skip、0 failpackage boundary 仍为 18 个 package 且无 single-source/shallow-source packagecluster dependency、edge import、cluster deployment 均零 finding。Edge arm64 本机观测模块加载 RSS `+8,945,664` bytes、1 万行输出峰值 `+5,226,496` bytes,不冒充固定物理低配门。PostgreSQL `18.4` arm64 physical HA 125 项、timeline `1→2` 通过,报告 SHA-256 `45fab400eb449774d50429103dd766a2755166530ac54ddc1056f777bc16c15f`,临时 Docker 资源已清理。
- 后续 controller/admission 切片取代上一条“自动 controller 仍未启用”的历史描述:生产 executor 已接入确定性 create/get-only Kubernetes Job controller。它以 durable approval/execution 为唯一创建依据;CREATE 的 409 或响应丢失通过 exact GET 收敛,`executing` 但 Job 缺失、approval 过期、终态 Job 或 contract 漂移均进入 `recoveryRequired`,不自动重建可能已经产生外部效果的动作。常规 dispatcher 只查询已注册 action type,因此不会误领取 Secret action。
- Controller ServiceAccount 只具有 Job `create|get`action ServiceAccount 无 API token且没有 Job/Pod/Secret 权限。`admissionregistration.k8s.io/v1` ValidatingAdmissionPolicy 以请求者身份和固定参数 ConfigMap 锁死 digest 镜像、command、ServiceAccount、source Secret、exact SHA-256 item/path、PostgreSQL SecretRef、安全上下文、资源与 volume/mount`failurePolicy: Fail``parameterNotFoundAction: Deny`。基础 NetworkPolicy 保持 DNS-only,生产集群必须用私有 overlay 显式加入 API Server 精确 CIDR/TCP 443 出口,避免为 controller 放开任意公网。
- 真实 K3s `v1.34.3+k3s1` 现场门已证明策略可由 API Server 编译:合规 Job dry-run 被接受,镜像漂移和参数 ConfigMap 缺失被拒绝;controller SA 的 `list|watch|delete jobs`、Pod 创建和 Secret 读取均被拒绝,action SA 的 Job/Pod 创建和 Secret 读取也均被拒绝。实现没有新增 workspace package、Edge daemon/timer/watcher 或低配设备常驻负担,18-package boundary 继续为 `singleSourcePackages=[]``shallowSourcePackages=[]`
- 本切片完整性门:controller/renderer/process 定向 18/18cluster-admin 339 pass/3 条件 skip、cluster-postgres 328 pass/2 条件 skip;完整 18-package clean build/test 退出 0backend 1196 项为 1194 pass、2 条条件 skip、0 failpackage boundary、cluster dependency、edge import、cluster deployment 均零 finding。PostgreSQL `18.4` arm64 physical HA 125 项、timeline `1→2` 通过,报告 SHA-256 `a3d34e61ea2064e1cde574e533137186e09fdce9048455da64f582906037fa0d`,临时 Docker 资源已清理。
- ADR 继续保持 Proposed:升级失败自动回滚、终态 Job 的 durable 恢复决议和固定物理低配设备证据尚未完成。当前 controller 明确暴露恢复要求,不以不安全的自动重试冒充闭环。
+5
View File
@@ -225,6 +225,11 @@
"require": "./dist/plugin-package/executor/pluginPackageKubernetesSecretActionJob.js",
"default": "./dist/plugin-package/executor/pluginPackageKubernetesSecretActionJob.js"
},
"./plugin-package-kubernetes-secret-action-controller": {
"types": "./dist/plugin-package/executor/pluginPackageKubernetesSecretActionController.d.ts",
"require": "./dist/plugin-package/executor/pluginPackageKubernetesSecretActionController.js",
"default": "./dist/plugin-package/executor/pluginPackageKubernetesSecretActionController.js"
},
"./prompt-output-gc-process": {
"types": "./dist/prompt-output/retention/promptOutputGcProcess.d.ts",
"require": "./dist/prompt-output/retention/promptOutputGcProcess.js",
@@ -15,6 +15,9 @@ import {
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
PostgresApprovedActionExecutionRepository,
PostgresPluginPackageSecretBindingApprovalPlanReader,
PostgresPluginPackageSecretBindingTransitionApprovalPlanReader,
type PostgresConnectionOptions,
type PostgresPoolOptions,
type PostgresSchemaReadinessReport,
@@ -48,6 +51,12 @@ import { ProjectedPluginPackageSecretExistenceInspector } from '../secret-bindin
import {
runClusterPluginPackagePublisherRevocation,
} from '../publisher/pluginPackagePublisherRevocation';
import {
createClusterPluginPackageKubernetesSecretActionController,
type ClusterPluginPackageKubernetesSecretActionControllerResource,
type PluginPackageKubernetesSecretActionControllerSummary,
} from './pluginPackageKubernetesSecretActionController';
import type { PluginPackageKubernetesSecretActionJobOptions } from './pluginPackageKubernetesSecretActionJob';
export type ClusterPluginPackageExecutorProcessEnvironment = Readonly<
Record<string, string | undefined>
@@ -66,6 +75,11 @@ export type ClusterPluginPackageExecutorProcessConfig =
revocationMaxPages: number;
dispatchId: string | null;
secretProjectionRoot: string | null;
kubernetesSecretActions: Readonly<{
enabled: true;
limit: number;
job: Readonly<PluginPackageKubernetesSecretActionJobOptions>;
}> | null;
database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
@@ -78,6 +92,7 @@ export interface ClusterPluginPackageExecutorBatchResult {
readonly secretBindingApprovals: Readonly<ClusterPluginPackageSecretBindingApprovalSummary>;
readonly secretBindingTransitionApprovals: Readonly<ClusterPluginPackageSecretBindingTransitionApprovalSummary>;
readonly dispatch: Readonly<ApprovedActionDispatchBatchSummary>;
readonly secretActionJobs: Readonly<PluginPackageKubernetesSecretActionControllerSummary>;
}
export type ClusterPluginPackageExecutorProcessResult =
@@ -116,6 +131,13 @@ export interface RunClusterPluginPackageExecutorProcessOptions {
pool: PostgresPool,
) => Promise<PostgresSchemaReadinessReport>;
readonly now?: () => number;
readonly createSecretActionController?: (
options: Parameters<
typeof createClusterPluginPackageKubernetesSecretActionController
>[0],
) => Promise<
Readonly<ClusterPluginPackageKubernetesSecretActionControllerResource>
>;
}
export class ClusterPluginPackageExecutorProcessConfigError extends TypeError {
@@ -131,6 +153,11 @@ export class ClusterPluginPackageExecutorProcessConfigError extends TypeError {
const SAFE_OWNER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SAFE_DISPATCH_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const DNS_NAME = /^(?=.{1,253}$)[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/;
const SECRET_KEY = /^[A-Za-z0-9._-]{1,253}$/;
const IMAGE_DIGEST =
/^[a-z0-9](?:[a-z0-9._:/-]{0,510}[a-z0-9])?@sha256:[0-9a-f]{64}$/;
function enabledValue(
environment: ClusterPluginPackageExecutorProcessEnvironment,
@@ -307,6 +334,179 @@ function secretProjectionRoot(
return value;
}
function requiredEnvironment(
environment: ClusterPluginPackageExecutorProcessEnvironment,
name: string,
maximumLength: number,
): string {
const value = boundedValue(environment, name, maximumLength);
if (value === undefined) {
throw new ClusterPluginPackageExecutorProcessConfigError(
`${name} is required when the Kubernetes Secret action controller is enabled`,
);
}
return value;
}
function kubernetesSecretActions(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): Readonly<{
enabled: true;
limit: number;
job: Readonly<PluginPackageKubernetesSecretActionJobOptions>;
}> | null {
const enabled = environment.QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED;
if (enabled === undefined || enabled === '' || enabled === 'false') return null;
if (enabled !== 'true') {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED must be true or false',
);
}
const namespace =
boundedValue(environment, 'QL3_PLUGIN_PACKAGE_SECRET_ACTION_NAMESPACE', 63) ??
'qinglong3-system';
const serviceAccountName =
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_SERVICE_ACCOUNT',
63,
) ?? 'ql3-plugin-package-secret-action';
const sourceSecretName =
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_SOURCE_SECRET',
63,
) ?? 'ql3-cluster-plugin-package-values';
const image = requiredEnvironment(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE',
640,
);
const postgresCaSecretName = requiredEnvironment(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_SECRET',
63,
);
const postgresCaKey =
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_KEY',
253,
) ?? 'ca.crt';
const servername = requiredEnvironment(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_SERVERNAME',
253,
);
const urlSecretName = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET',
63,
);
const urlSecretKey = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY',
253,
);
const fieldNames = [
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_AUTH_SECRET',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_HOST',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PORT',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_DATABASE',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_USERNAME_KEY',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PASSWORD_KEY',
] as const;
const fields = fieldNames.map((name) => boundedValue(environment, name, 253));
const urlMode = urlSecretName !== undefined || urlSecretKey !== undefined;
const fieldMode = fields.some((value) => value !== undefined);
if (
!DNS_LABEL.test(namespace) ||
!DNS_LABEL.test(serviceAccountName) ||
!DNS_LABEL.test(sourceSecretName) ||
!IMAGE_DIGEST.test(image) ||
!DNS_LABEL.test(postgresCaSecretName) ||
!SECRET_KEY.test(postgresCaKey) ||
!DNS_NAME.test(servername) ||
urlMode === fieldMode
) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action Job identity is invalid',
);
}
let connection: PluginPackageKubernetesSecretActionJobOptions['postgres']['connection'];
if (urlMode) {
if (
urlSecretName === undefined ||
urlSecretKey === undefined ||
!DNS_LABEL.test(urlSecretName) ||
!SECRET_KEY.test(urlSecretKey)
) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action PostgreSQL URL reference is invalid',
);
}
connection = Object.freeze({
mode: 'url',
secretName: urlSecretName,
urlKey: urlSecretKey,
});
} else {
if (fields.some((value) => value === undefined)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action PostgreSQL field reference is incomplete',
);
}
const [authSecretName, host, portValue, database, usernameKey, passwordKey] =
fields as [string, string, string, string, string, string];
const port = Number(portValue);
if (
!DNS_LABEL.test(authSecretName) ||
!DNS_NAME.test(host) ||
!Number.isSafeInteger(port) ||
port < 1 ||
port > 65_535 ||
!/^[A-Za-z_][A-Za-z0-9_$-]{0,62}$/.test(database) ||
!SECRET_KEY.test(usernameKey) ||
!SECRET_KEY.test(passwordKey)
) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action PostgreSQL field reference is invalid',
);
}
connection = Object.freeze({
mode: 'fields',
authSecretName,
host,
port,
database,
usernameKey,
passwordKey,
});
}
return Object.freeze({
enabled: true,
limit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_LIMIT',
8,
1,
32,
),
job: Object.freeze({
namespace,
serviceAccountName,
sourceSecretName,
image,
postgres: Object.freeze({
connection,
caSecretName: postgresCaSecretName,
caKey: postgresCaKey,
servername,
}),
}),
});
}
export function loadClusterPluginPackageExecutorProcessConfig(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): ClusterPluginPackageExecutorProcessConfig {
@@ -382,6 +582,7 @@ export function loadClusterPluginPackageExecutorProcessConfig(
),
dispatchId,
secretProjectionRoot: secretProjectionRoot(environment),
kubernetesSecretActions: kubernetesSecretActions(environment),
database: databaseConfig(environment),
});
}
@@ -424,6 +625,18 @@ function emptyApprovalSummary(): Readonly<{
});
}
function emptySecretActionJobSummary(): Readonly<PluginPackageKubernetesSecretActionControllerSummary> {
return Object.freeze({
scanned: 0,
created: 0,
existing: 0,
active: 0,
recoveryRequired: 0,
unavailable: 0,
truncated: false,
});
}
export async function runClusterPluginPackageExecutorProcess(
options: RunClusterPluginPackageExecutorProcessOptions,
): Promise<ClusterPluginPackageExecutorProcessResult> {
@@ -446,6 +659,8 @@ export async function runClusterPluginPackageExecutorProcess(
typeof options.createDispatcher !== 'function') ||
(options.assertReady !== undefined &&
typeof options.assertReady !== 'function') ||
(options.createSecretActionController !== undefined &&
typeof options.createSecretActionController !== 'function') ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError('Plugin Package executor process options are invalid');
@@ -464,6 +679,9 @@ export async function runClusterPluginPackageExecutorProcess(
});
const database = await openDatabase();
let failure: unknown;
let secretActionControllerResource:
| Readonly<ClusterPluginPackageKubernetesSecretActionControllerResource>
| undefined;
try {
const evidence = await (
options.assertReady ?? assertPostgresPackageExecutorSchemaReady
@@ -531,6 +749,7 @@ export async function runClusterPluginPackageExecutorProcess(
secretBindingApprovals: emptyApprovalSummary(),
secretBindingTransitionApprovals: emptyApprovalSummary(),
dispatch,
secretActionJobs: emptySecretActionJobSummary(),
}),
]),
});
@@ -559,6 +778,32 @@ export async function runClusterPluginPackageExecutorProcess(
limit: config.approvalBatchSize,
...(options.now ? { now: options.now } : {}),
});
let secretActionJobs = emptySecretActionJobSummary();
if (config.kubernetesSecretActions !== null) {
if (!secretActionControllerResource) {
const createController =
options.createSecretActionController ??
createClusterPluginPackageKubernetesSecretActionController;
secretActionControllerResource = await createController({
executions: new PostgresApprovedActionExecutionRepository(
database.pool,
),
bindingPlans:
new PostgresPluginPackageSecretBindingApprovalPlanReader(
database.pool,
),
transitionPlans:
new PostgresPluginPackageSecretBindingTransitionApprovalPlanReader(
database.pool,
),
job: config.kubernetesSecretActions.job,
...(options.now ? { now: options.now } : {}),
});
}
secretActionJobs = await secretActionControllerResource.controller.reconcile({
limit: config.kubernetesSecretActions.limit,
});
}
const dispatch = await dispatcher.dispatchBatch({
limit: config.dispatchBatchSize,
});
@@ -568,9 +813,10 @@ export async function runClusterPluginPackageExecutorProcess(
secretBindingApprovals,
secretBindingTransitionApprovals,
dispatch,
secretActionJobs,
});
batches.push(batch);
if (isIdleBatch(batch)) break;
if (secretActionJobs.scanned > 0 || isIdleBatch(batch)) break;
}
return Object.freeze({
status: 'completed',
@@ -581,16 +827,33 @@ export async function runClusterPluginPackageExecutorProcess(
failure = error;
throw error;
} finally {
let disposeError: unknown;
try {
secretActionControllerResource?.dispose();
} catch (error) {
disposeError = error;
}
let closeError: unknown;
try {
await database.close();
} catch (closeError) {
} catch (error) {
closeError = error;
}
const cleanupErrors = [disposeError, closeError].filter(
(error) => error !== undefined,
);
if (cleanupErrors.length > 0) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Plugin Package executor process failed and PostgreSQL did not close',
[failure, ...cleanupErrors],
'Plugin Package executor process and resource cleanup failed',
);
}
throw closeError;
if (cleanupErrors.length === 1) throw cleanupErrors[0];
throw new AggregateError(
cleanupErrors,
'Plugin Package executor resource cleanup failed',
);
}
}
}
@@ -0,0 +1,403 @@
import type { ApprovedActionExecutionSnapshot } from '@qinglong/runtime-core/approved-action-execution';
import {
PLUGIN_PACKAGE_SECRET_BINDING_ACTION_TYPE,
type PluginPackageSecretBindingApprovalPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-approval-plan';
import {
PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_ACTION_TYPE,
type PluginPackageSecretBindingTransitionApprovalPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-approval-plan';
import {
createPluginPackageKubernetesSecretActionJob,
type PluginPackageKubernetesSecretActionJobOptions,
} from './pluginPackageKubernetesSecretActionJob';
const FIELD_MANAGER = 'qinglong-plugin-package-secret-action-controller';
const MAX_PAGE_SIZE = 32;
type SecretActionApprovalPlan =
| PluginPackageSecretBindingApprovalPlan
| PluginPackageSecretBindingTransitionApprovalPlan;
type JsonObject = Record<string, unknown>;
export interface PluginPackageSecretActionApprovalPlanReader<T> {
findByActionRef(actionRef: string): Promise<Readonly<T> | null>;
}
export interface PluginPackageKubernetesSecretActionExecutionReader {
listReconciliableExecutions(query: Readonly<{
nowMs: number;
limit: number;
actionTypes: readonly string[];
}>): Promise<Readonly<{
executions: readonly Readonly<ApprovedActionExecutionSnapshot>[];
truncated: boolean;
}>>;
}
export interface PluginPackageKubernetesSecretActionJobResource {
readonly metadata?: Readonly<{
name?: string;
namespace?: string;
labels?: Readonly<Record<string, string>>;
annotations?: Readonly<Record<string, string>>;
}>;
readonly spec?: Readonly<Record<string, unknown>>;
readonly status?: Readonly<{
conditions?: readonly Readonly<{
type?: string;
status?: string;
}>[];
}>;
}
export interface PluginPackageKubernetesSecretActionJobApi {
createNamespacedJob(
request: Readonly<{
namespace: string;
body: Readonly<Record<string, unknown>>;
fieldManager: typeof FIELD_MANAGER;
fieldValidation: 'Strict';
}>,
): Promise<PluginPackageKubernetesSecretActionJobResource>;
readNamespacedJob(
request: Readonly<{
name: string;
namespace: string;
}>,
): Promise<PluginPackageKubernetesSecretActionJobResource>;
}
export interface PluginPackageKubernetesSecretActionControllerOptions {
readonly executions: PluginPackageKubernetesSecretActionExecutionReader;
readonly bindingPlans: PluginPackageSecretActionApprovalPlanReader<PluginPackageSecretBindingApprovalPlan>;
readonly transitionPlans: PluginPackageSecretActionApprovalPlanReader<PluginPackageSecretBindingTransitionApprovalPlan>;
readonly jobs: PluginPackageKubernetesSecretActionJobApi;
readonly job: Readonly<PluginPackageKubernetesSecretActionJobOptions>;
readonly now?: () => number;
}
export interface PluginPackageKubernetesSecretActionControllerSummary {
readonly scanned: number;
readonly created: number;
readonly existing: number;
readonly active: number;
readonly recoveryRequired: number;
readonly unavailable: number;
readonly truncated: boolean;
}
export interface ClusterPluginPackageKubernetesSecretActionControllerResource {
readonly controller: PluginPackageKubernetesSecretActionController;
dispose(): void;
}
export class PluginPackageKubernetesSecretActionControllerConflictError extends Error {
readonly code = 'PLUGIN_PACKAGE_KUBERNETES_SECRET_ACTION_CONTROLLER_CONFLICT';
constructor() {
super('Kubernetes Secret action Job conflicts with the durable dispatch');
this.name = 'PluginPackageKubernetesSecretActionControllerConflictError';
}
}
export class PluginPackageKubernetesSecretActionControllerUnavailableError extends Error {
readonly code =
'PLUGIN_PACKAGE_KUBERNETES_SECRET_ACTION_CONTROLLER_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Kubernetes Secret action Job authority is unavailable', options);
this.name =
'PluginPackageKubernetesSecretActionControllerUnavailableError';
}
}
function apiStatus(error: unknown): number | null {
if (!error || typeof error !== 'object') return null;
if ('code' in error && typeof error.code === 'number') return error.code;
if (
'response' in error &&
error.response &&
typeof error.response === 'object' &&
'statusCode' in error.response &&
typeof error.response.statusCode === 'number'
) {
return error.response.statusCode;
}
return null;
}
function object(value: unknown): JsonObject | null {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as JsonObject)
: null;
}
function expectedSubset(expected: unknown, observed: unknown): boolean {
if (Array.isArray(expected)) {
return (
Array.isArray(observed) &&
expected.length === observed.length &&
expected.every((value, index) => expectedSubset(value, observed[index]))
);
}
const expectedObject = object(expected);
if (expectedObject) {
const observedObject = object(observed);
return (
observedObject !== null &&
Object.entries(expectedObject).every(([key, value]) =>
expectedSubset(value, observedObject[key]),
)
);
}
return Object.is(expected, observed);
}
function terminalStatus(
job: Readonly<PluginPackageKubernetesSecretActionJobResource>,
): 'active' | 'complete' | 'failed' {
const conditions = job.status?.conditions ?? [];
const complete = conditions.some(
(condition) => condition.type === 'Complete' && condition.status === 'True',
);
const failed = conditions.some(
(condition) => condition.type === 'Failed' && condition.status === 'True',
);
if (complete && failed) {
throw new PluginPackageKubernetesSecretActionControllerConflictError();
}
if (complete) return 'complete';
if (failed) return 'failed';
return 'active';
}
function assertObservedJob(
expected: Readonly<Record<string, unknown>>,
observed: Readonly<PluginPackageKubernetesSecretActionJobResource>,
): void {
const expectedMetadata = object(expected.metadata)!;
const expectedSpec = object(expected.spec)!;
const observedMetadata = observed.metadata;
const observedPodSpec = object(
object(object(observed.spec)?.template)?.spec,
);
if (
observedMetadata?.name !== expectedMetadata.name ||
observedMetadata?.namespace !== expectedMetadata.namespace ||
!expectedSubset(expectedMetadata.labels, observedMetadata?.labels) ||
!expectedSubset(expectedMetadata.annotations, observedMetadata?.annotations) ||
!expectedSubset(expectedSpec, observed.spec) ||
observedPodSpec === null ||
observedPodSpec.hostNetwork === true ||
observedPodSpec.hostPID === true ||
observedPodSpec.hostIPC === true ||
(Array.isArray(observedPodSpec.initContainers) &&
observedPodSpec.initContainers.length > 0) ||
(Array.isArray(observedPodSpec.ephemeralContainers) &&
observedPodSpec.ephemeralContainers.length > 0)
) {
throw new PluginPackageKubernetesSecretActionControllerConflictError();
}
}
export class PluginPackageKubernetesSecretActionController {
readonly #now: () => number;
constructor(
private readonly options: PluginPackageKubernetesSecretActionControllerOptions,
) {
if (
!options ||
typeof options !== 'object' ||
!options.executions ||
typeof options.executions.listReconciliableExecutions !== 'function' ||
!options.bindingPlans ||
typeof options.bindingPlans.findByActionRef !== 'function' ||
!options.transitionPlans ||
typeof options.transitionPlans.findByActionRef !== 'function' ||
!options.jobs ||
typeof options.jobs.createNamespacedJob !== 'function' ||
typeof options.jobs.readNamespacedJob !== 'function' ||
!options.job ||
typeof options.job !== 'object' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError('Kubernetes Secret action controller options are invalid');
}
this.#now = options.now ?? Date.now;
}
async reconcile(
input: Readonly<{ limit?: number }> = {},
): Promise<Readonly<PluginPackageKubernetesSecretActionControllerSummary>> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).some((key) => key !== 'limit')
) {
throw new TypeError('Kubernetes Secret action reconciliation is invalid');
}
const limit = input.limit ?? 8;
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_SIZE) {
throw new RangeError('Kubernetes Secret action reconciliation limit is invalid');
}
const nowMs = this.#now();
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
throw new RangeError('Kubernetes Secret action controller clock is invalid');
}
let page;
try {
page = await this.options.executions.listReconciliableExecutions({
nowMs,
limit,
actionTypes: Object.freeze([
PLUGIN_PACKAGE_SECRET_BINDING_ACTION_TYPE,
PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_ACTION_TYPE,
]),
});
} catch (error) {
throw new PluginPackageKubernetesSecretActionControllerUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
const summary = {
scanned: page.executions.length,
created: 0,
existing: 0,
active: 0,
recoveryRequired: 0,
unavailable: 0,
truncated: page.truncated,
};
for (const snapshot of page.executions) {
try {
const result = await this.#reconcileOne(snapshot, nowMs);
summary[result] += 1;
} catch (error) {
if (
error instanceof
PluginPackageKubernetesSecretActionControllerConflictError
) {
throw error;
}
summary.unavailable += 1;
}
}
return Object.freeze({ ...summary });
}
async #reconcileOne(
snapshot: Readonly<ApprovedActionExecutionSnapshot>,
nowMs: number,
): Promise<'created' | 'existing' | 'active' | 'recoveryRequired'> {
const plan = await this.#plan(snapshot);
if (!plan) {
throw new PluginPackageKubernetesSecretActionControllerUnavailableError();
}
const desired = createPluginPackageKubernetesSecretActionJob({
dispatch: snapshot.dispatch,
approvalPlan: plan,
options: this.options.job,
});
const metadata = object(desired.metadata)!;
const name = metadata.name as string;
const namespace = metadata.namespace as string;
let observed: PluginPackageKubernetesSecretActionJobResource;
let disposition: 'created' | 'existing' | 'active' = 'active';
try {
observed = await this.options.jobs.readNamespacedJob({ name, namespace });
} catch (error) {
if (apiStatus(error) !== 404) throw error;
if (
snapshot.execution.status === 'executing' ||
nowMs > plan.expiresAtMs
) {
return 'recoveryRequired';
}
try {
observed = await this.options.jobs.createNamespacedJob({
namespace,
body: desired,
fieldManager: FIELD_MANAGER,
fieldValidation: 'Strict',
});
disposition = 'created';
} catch (createError) {
// CREATE may have succeeded even when its response was lost. Converge
// every ambiguous failure through an exact-name GET before surfacing it.
try {
observed = await this.options.jobs.readNamespacedJob({
name,
namespace,
});
} catch (readAfterCreateError) {
if (apiStatus(readAfterCreateError) === 404) throw createError;
throw readAfterCreateError;
}
disposition = 'existing';
}
}
assertObservedJob(desired, observed);
const terminal = terminalStatus(observed);
if (terminal !== 'active') return 'recoveryRequired';
return disposition;
}
#plan(
snapshot: Readonly<ApprovedActionExecutionSnapshot>,
): Promise<Readonly<SecretActionApprovalPlan> | null> {
const { actionType, actionRef } = snapshot.dispatch.action;
if (actionType === PLUGIN_PACKAGE_SECRET_BINDING_ACTION_TYPE) {
return this.options.bindingPlans.findByActionRef(actionRef);
}
if (actionType === PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_ACTION_TYPE) {
return this.options.transitionPlans.findByActionRef(actionRef);
}
return Promise.resolve(null);
}
}
type KubernetesModule = typeof import('@kubernetes/client-node', {
with: { 'resolution-mode': 'import' }
});
export async function createClusterPluginPackageKubernetesSecretActionController(
options: Omit<
PluginPackageKubernetesSecretActionControllerOptions,
'jobs'
>,
): Promise<Readonly<ClusterPluginPackageKubernetesSecretActionControllerResource>> {
const kubernetes = (await import('@kubernetes/client-node')) as KubernetesModule;
const config = new kubernetes.KubeConfig();
config.loadFromCluster();
const jobs = config.makeApiClient(
kubernetes.BatchV1Api,
) as unknown as PluginPackageKubernetesSecretActionJobApi;
let active = true;
return Object.freeze({
controller: new PluginPackageKubernetesSecretActionController({
...options,
jobs,
}),
dispose() {
if (!active) return;
active = false;
for (const user of config.getUsers()) {
const mutable = user as {
token?: string;
certData?: string;
keyData?: string;
};
mutable.token = '';
mutable.certData = '';
mutable.keyData = '';
}
config.setCurrentContext('disposed');
},
});
}
@@ -27,6 +27,27 @@ function environment(overrides = {}) {
};
}
function actionControllerEnvironment(overrides = {}) {
return environment({
QL3_PLUGIN_PACKAGE_EXECUTOR_SECRET_ROOT: undefined,
QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED: 'true',
QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_LIMIT: '6',
QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE:
'registry.example.com/qinglong/qinglong3-cluster-admin@sha256:' +
'c'.repeat(64),
QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_SECRET:
'ql3-cluster-plugin-package-executor',
QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_KEY: 'postgres-ca.crt',
QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_SERVERNAME:
'postgres.qinglong3-system.svc',
QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET:
'ql3-cluster-plugin-package-executor',
QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY:
'postgres-package-executor-url',
...overrides,
});
}
test('disabled executor opens no PostgreSQL authority', async () => {
let opened = 0;
const result = await runClusterPluginPackageExecutorProcess({
@@ -74,6 +95,27 @@ test('loads one bounded action-scoped dispatch without widening batch limits', (
assert.equal(config.maxBatches, 4);
});
test('loads a bounded digest-pinned Kubernetes Secret action controller', () => {
const config = loadClusterPluginPackageExecutorProcessConfig(
actionControllerEnvironment(),
);
assert.equal(config.enabled, true);
assert.equal(config.secretProjectionRoot, null);
assert.equal(config.kubernetesSecretActions.limit, 6);
assert.equal(
config.kubernetesSecretActions.job.serviceAccountName,
'ql3-plugin-package-secret-action',
);
assert.equal(
config.kubernetesSecretActions.job.postgres.connection.mode,
'url',
);
assert.equal(
config.kubernetesSecretActions.job.postgres.connection.secretName,
'ql3-cluster-plugin-package-executor',
);
});
test('action-scoped mode skips every Approval consumer and shared queue scan', async () => {
const calls = [];
const pool = {};
@@ -151,6 +193,129 @@ test('action-scoped mode skips every Approval consumer and shared queue scan', a
assert.equal(result.batches[0].dispatch.succeeded, 1);
});
test('batch mode consumes approvals before reconciling exact Secret action Jobs', async () => {
const calls = [];
const pool = {
async query() {
throw new Error('repositories are injected behind the controller factory');
},
async connect() {
throw new Error('repositories are injected behind the controller factory');
},
};
const approvalSummary = {
scanned: 0,
consumed: 0,
existing: 0,
expired: 0,
blocked: 0,
};
const readiness = {
ready: true,
writablePrimary: true,
serverVersionNum: 180004,
serverMajor: 18,
currentUser: 'ql3_package_executor',
contractName: 'control-core',
contractVersion: 62,
migrationIds: ['pg-0063-plugin-package-secret-binding-transition-receipts'],
};
const result = await runClusterPluginPackageExecutorProcess({
environment: actionControllerEnvironment(),
async openDatabase() {
calls.push('open');
return {
pool,
async close() {
calls.push('close');
},
};
},
async assertReady() {
calls.push('ready');
return readiness;
},
async consumeApprovals() {
calls.push('publisher-approvals');
return approvalSummary;
},
async consumeTrustTransitionApprovals() {
calls.push('trust-approvals');
return approvalSummary;
},
async consumeSecretBindingApprovals() {
calls.push('binding-approvals');
return { ...approvalSummary, scanned: 1, consumed: 1 };
},
async consumeSecretBindingTransitionApprovals() {
calls.push('transition-approvals');
return approvalSummary;
},
async createSecretActionController(options) {
assert.equal(options.job.image.endsWith('c'.repeat(64)), true);
calls.push('controller-open');
return {
controller: {
async reconcile({ limit }) {
calls.push(`reconcile:${limit}`);
return {
scanned: 1,
created: 1,
existing: 0,
active: 0,
recoveryRequired: 0,
unavailable: 0,
truncated: false,
};
},
},
dispose() {
calls.push('controller-close');
},
};
},
createDispatcher() {
return {
async dispatchById() {
throw new Error('batch mode must not exact dispatch');
},
async dispatchBatch() {
calls.push('dispatch-batch');
return {
scanned: 0,
claimed: 0,
started: 0,
succeeded: 0,
failed: 0,
blocked: 0,
retrying: 0,
deferred: 0,
recoveryRequired: 0,
alreadyTerminal: 0,
unavailable: 0,
truncated: false,
};
},
};
},
});
assert.equal(result.batches.length, 1);
assert.equal(result.batches[0].secretActionJobs.created, 1);
assert.deepEqual(calls, [
'open',
'ready',
'publisher-approvals',
'trust-approvals',
'binding-approvals',
'transition-approvals',
'controller-open',
'reconcile:6',
'dispatch-batch',
'controller-close',
'close',
]);
});
test('rejects implicit insecure PostgreSQL and unbounded work', () => {
for (const invalid of [
environment({ QL3_POSTGRES_ALLOW_INSECURE: undefined }),
@@ -161,6 +326,12 @@ test('rejects implicit insecure PostgreSQL and unbounded work', () => {
environment({
QL3_PLUGIN_PACKAGE_EXECUTOR_DISPATCH_ID: 'dispatch id with spaces',
}),
actionControllerEnvironment({
QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE: 'tag-only:latest',
}),
actionControllerEnvironment({
QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET: undefined,
}),
]) {
assert.throws(
() => loadClusterPluginPackageExecutorProcessConfig(invalid),
@@ -0,0 +1,366 @@
'use strict';
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
consumeApprovalRequest,
createApprovalRequest,
decideApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createApprovedActionExecution,
} = require('@qinglong/runtime-core/approved-action-execution');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
createPluginPackageSecretBindingApprovalPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-approval-plan');
const {
createPluginPackageSecretBindingPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-plan');
const {
createSecretRef,
} = require('@qinglong/runtime-core/secret-reference');
const {
PluginPackageKubernetesSecretActionController,
PluginPackageKubernetesSecretActionControllerConflictError,
} = require('@qinglong/cluster-admin/plugin-package-kubernetes-secret-action-controller');
const REQUESTER = Object.freeze({ type: 'user', id: 'cluster-owner' });
const REVIEWER = Object.freeze({ type: 'user', id: 'security-reviewer' });
const CONSUMER = Object.freeze({
type: 'system',
id: 'cluster_package_executor',
});
const FENCE = Object.freeze({ projectVersion: 3, bindingVersion: 4 });
function fixture() {
const manifest = {
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: {
name: 'controller-fixture',
displayName: 'Controller Fixture',
version: '1.0.0',
description: 'Secret action controller fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['cluster-control'],
},
runtimes: [],
resources: {
memory: { recommended: '32Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [{ name: 'TOKEN', required: true }],
tools: ['secret.use'],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
const generation = createPluginPackageResourceGeneration({
installationId: 'install-controller-1',
projectId: 'project-1',
packageName: 'controller-fixture',
lockDigest: 'a'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: 'b'.repeat(64),
contents: manifest.spec.contents,
});
const bindingPlan = createPluginPackageSecretBindingPlan({
generation,
manifest,
assignments: [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'runtime-token',
version: 2,
}),
},
],
plannedAtMs: 100,
});
const plan = createPluginPackageSecretBindingApprovalPlan({
actionRef: 'secret-binding:controller-fixture',
bindingPlan,
requestedBy: REQUESTER,
expiresAtMs: 1_000,
});
const action = require('@qinglong/runtime-core/plugin-package-secret-binding-approval-plan')
.pluginPackageSecretBindingApprovedAction(plan);
const approved = decideApprovalRequest(
createApprovalRequest({
id: 'approval-controller-1',
projectId: 'project-1',
action,
risk: 'high',
decisionMode: 'separation_of_duty',
requestedBy: REQUESTER,
requestedAtMs: 110,
expiresAtMs: 900,
requestFence: FENCE,
}),
{
expectedVersion: 1,
decisionId: 'decision-controller-1',
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: REVIEWER,
authenticationId: 'auth-reviewer',
authenticatedAtMs: 100,
expiresAtMs: 800,
assurance: 'multi_factor',
},
decidedAtMs: 120,
authorizationFence: FENCE,
},
);
const dispatch = consumeApprovalRequest(approved, {
expectedVersion: 2,
consumptionId: 'consume-controller-1',
dispatchId: 'dispatch-controller-1',
action,
requestedBy: REQUESTER,
consumedBy: CONSUMER,
consumedAtMs: 130,
authorizationFence: FENCE,
}).dispatch;
return {
plan,
snapshot: Object.freeze({
dispatch,
execution: createApprovedActionExecution(dispatch),
}),
};
}
function jobOptions() {
return {
namespace: 'qinglong3-system',
serviceAccountName: 'ql3-plugin-package-secret-action',
sourceSecretName: 'ql3-cluster-plugin-package-values',
image:
'registry.example.com/qinglong/qinglong3-cluster-admin@sha256:' +
'c'.repeat(64),
postgres: {
connection: {
mode: 'url',
secretName: 'ql3-cluster-plugin-package-executor',
urlKey: 'postgres-package-executor-url',
},
caSecretName: 'ql3-cluster-plugin-package-executor',
caKey: 'postgres-ca.crt',
servername: 'postgres.qinglong3-system.svc',
},
};
}
function apiError(code) {
return Object.assign(new Error(`Kubernetes ${code}`), { code });
}
function controller({ read, create, now = 200, snapshot: snapshotOverride }) {
const { plan, snapshot } = fixture();
return new PluginPackageKubernetesSecretActionController({
executions: {
async listReconciliableExecutions(query) {
assert.deepEqual(query.actionTypes, [
'plugin_package.secret_binding.bind',
'plugin_package.secret_binding.transition',
]);
return {
executions: [snapshotOverride ?? snapshot],
truncated: false,
};
},
},
bindingPlans: {
async findByActionRef(actionRef) {
assert.equal(actionRef, plan.actionRef);
return plan;
},
},
transitionPlans: {
async findByActionRef() {
throw new Error('transition reader must not run');
},
},
jobs: {
readNamespacedJob: read,
createNamespacedJob: create,
},
job: jobOptions(),
now: () => now,
});
}
test('creates one Strict deterministic Job without claiming the execution', async () => {
const calls = [];
const subject = controller({
async read(request) {
calls.push(['read', request]);
throw apiError(404);
},
async create(request) {
calls.push(['create', request]);
return request.body;
},
});
const result = await subject.reconcile({ limit: 4 });
assert.equal(result.created, 1);
assert.equal(result.recoveryRequired, 0);
assert.equal(calls[0][0], 'read');
assert.equal(calls[1][0], 'create');
assert.equal(calls[1][1].fieldValidation, 'Strict');
assert.equal(
calls[1][1].fieldManager,
'qinglong-plugin-package-secret-action-controller',
);
assert.match(calls[1][1].body.metadata.name, /^ql3-package-secret-[0-9a-f]{32}$/);
});
test('converges a concurrent create through one exact get', async () => {
let desired;
let reads = 0;
const subject = controller({
async read() {
reads += 1;
if (reads === 1) throw apiError(404);
return {
...desired,
metadata: {
...desired.metadata,
uid: 'server-owned-uid',
},
spec: {
...desired.spec,
completionMode: 'NonIndexed',
},
};
},
async create(request) {
desired = request.body;
throw apiError(409);
},
});
const result = await subject.reconcile();
assert.equal(result.existing, 1);
assert.equal(reads, 2);
});
test('converges a lost successful CREATE response through one exact get', async () => {
let desired;
let reads = 0;
const subject = controller({
async read() {
reads += 1;
if (reads === 1) throw apiError(404);
return desired;
},
async create(request) {
desired = request.body;
throw apiError(503);
},
});
const result = await subject.reconcile();
assert.equal(result.existing, 1);
assert.equal(result.unavailable, 0);
assert.equal(reads, 2);
});
test('does not recreate a missing Job for an already executing action', async () => {
const { snapshot } = fixture();
let creates = 0;
const subject = controller({
snapshot: {
...snapshot,
execution: { ...snapshot.execution, status: 'executing' },
},
async read() {
throw apiError(404);
},
async create() {
creates += 1;
throw new Error('must not create');
},
});
const result = await subject.reconcile();
assert.equal(result.recoveryRequired, 1);
assert.equal(creates, 0);
});
test('does not create a missing Job after the approval plan expires', async () => {
let creates = 0;
const subject = controller({
now: 1_001,
async read() {
throw apiError(404);
},
async create() {
creates += 1;
throw new Error('must not create');
},
});
const result = await subject.reconcile();
assert.equal(result.recoveryRequired, 1);
assert.equal(creates, 0);
});
test('marks a terminal Job with a nonterminal execution as recovery required', async () => {
let desired;
const subject = controller({
async read() {
if (!desired) throw apiError(404);
return desired;
},
async create(request) {
desired = {
...request.body,
status: {
conditions: [{ type: 'Failed', status: 'True' }],
},
};
return desired;
},
});
const result = await subject.reconcile();
assert.equal(result.recoveryRequired, 1);
assert.equal(result.created, 0);
});
test('fails closed when a deterministic Job name contains another contract', async () => {
const subject = controller({
async read() {
const error = apiError(404);
throw error;
},
async create(request) {
return {
...request.body,
metadata: {
...request.body.metadata,
annotations: {
...request.body.metadata.annotations,
'qinglong.io/secret-action-job-digest': 'f'.repeat(64),
},
},
};
},
});
await assert.rejects(
() => subject.reconcile(),
PluginPackageKubernetesSecretActionControllerConflictError,
);
});
@@ -379,6 +379,62 @@ export class PostgresApprovedActionExecutionRepository
}
}
async listReconciliableExecutions(
query: Readonly<{
nowMs: number;
limit: number;
actionTypes: readonly string[];
}>,
): Promise<Readonly<{
executions: readonly Readonly<ApprovedActionExecutionSnapshot>[];
truncated: boolean;
}>> {
assertPageSize(query.limit);
const handledActionTypes = actionTypes(query.actionTypes);
if (!Number.isSafeInteger(query.nowMs) || query.nowMs < 0) {
throw new ApprovedActionExecutionStateConflictError();
}
if (handledActionTypes.length === 0) {
return Object.freeze({ executions: Object.freeze([]), truncated: false });
}
try {
const result = await this.pool.query<Row>(
`SELECT execution.execution_json AS "executionJson",
execution.execution_digest AS "executionDigest",
dispatch.dispatch_json AS "dispatchJson"
FROM "ql3"."approved_action_executions" AS execution
JOIN "ql3"."approved_action_dispatches" AS dispatch
ON dispatch.dispatch_id = execution.dispatch_id
WHERE dispatch.action_type = ANY($1::varchar[])
AND (
(
execution.status IN ('pending','leased','retry_wait')
AND execution.eligible_at_ms <= $2
)
OR execution.status = 'executing'
)
ORDER BY execution.updated_at_ms, execution.dispatch_id
LIMIT $3`,
[handledActionTypes, query.nowMs, query.limit + 1],
);
const truncated = result.rows.length > query.limit;
const rows = truncated ? result.rows.slice(0, query.limit) : result.rows;
return Object.freeze({
executions: Object.freeze(
rows.map((row) =>
normalizeApprovedActionExecutionSnapshot({
dispatch: parseDispatch(row),
execution: parseExecution(row),
}),
),
),
truncated,
});
} catch (error) {
throw mappedError(error);
}
}
claimExecution(
command: ClaimApprovedActionExecutionCommand,
): Promise<ClaimApprovedActionExecutionResult> {
@@ -41,6 +41,7 @@ export {
export { PostgresPluginPackageMaterializedRevisionRepository } from '../plugin-package/installation/pluginPackageMaterializedRevisionRepository';
export { PostgresApprovalRequestRepository } from '../approved-action/approvalRequestRepository';
export { PostgresApprovedActionExecutionRepository } from '../approved-action/approvedActionExecutionRepository';
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
export { PostgresPluginPackageSecretBindingRepository } from '../plugin-package/installation/pluginPackageSecretBindingRepository';
export { PostgresPluginPackageSecretBindingActivationPrerequisite } from '../plugin-package/secret-binding/pluginPackageSecretBindingActivationPrerequisite';
@@ -367,11 +367,16 @@ test('Approved Action execution authority is isolated behind its explicit subpat
const runtime = require('@qinglong/cluster-postgres/runtime');
const admin = require('@qinglong/cluster-postgres/admin');
const ingress = require('@qinglong/cluster-postgres/worker-ingress');
const packageExecutor = require('@qinglong/cluster-postgres/package-executor');
const authority = require('@qinglong/cluster-postgres/approved-action-execution');
assert.equal(root.PostgresApprovedActionExecutionRepository, undefined);
assert.equal(runtime.PostgresApprovedActionExecutionRepository, undefined);
assert.equal(admin.PostgresApprovedActionExecutionRepository, undefined);
assert.equal(ingress.PostgresApprovedActionExecutionRepository, undefined);
assert.equal(
packageExecutor.PostgresApprovedActionExecutionRepository,
authority.PostgresApprovedActionExecutionRepository,
);
assert.equal(
typeof authority.PostgresApprovedActionExecutionRepository,
'function',
@@ -3581,6 +3581,22 @@ if (!migrationConnectionString) {
const executions = new PostgresApprovedActionExecutionRepository(
executorDatabase.pool,
);
const pendingSecretActions =
await executions.listReconciliableExecutions({
nowMs: claimedAtMs,
limit: 1,
actionTypes: [consumed.dispatch.action.actionType],
});
assert.equal(pendingSecretActions.truncated, false);
assert.equal(pendingSecretActions.executions.length, 1);
assert.equal(
pendingSecretActions.executions[0].dispatch.id,
consumed.dispatch.id,
);
assert.equal(
pendingSecretActions.executions[0].execution.status,
'pending',
);
const claimed = await executions.claimExecution({
dispatchId: consumed.dispatch.id,
owner: 'package_admission_dispatcher',
@@ -3598,6 +3614,22 @@ if (!migrationConnectionString) {
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: admittedAtMs,
});
const executingSecretActions =
await executions.listReconciliableExecutions({
nowMs: admittedAtMs,
limit: 1,
actionTypes: [consumed.dispatch.action.actionType],
});
assert.equal(executingSecretActions.truncated, false);
assert.equal(executingSecretActions.executions.length, 1);
assert.equal(
executingSecretActions.executions[0].dispatch.id,
consumed.dispatch.id,
);
assert.equal(
executingSecretActions.executions[0].execution.status,
'executing',
);
const lock = resolvePluginPackageInstallProposal(
proposal,
consumed.dispatch,
+193 -2
View File
@@ -3185,6 +3185,12 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
const base = path.join(operationRoot, 'base');
const expectedResources = [
'service-account.yaml',
'secret-action-service-account.yaml',
'secret-action-admission-config.yaml',
'role.yaml',
'role-binding.yaml',
'validating-admission-policy.yaml',
'validating-admission-policy-binding.yaml',
'cron-job.yaml',
'network-policy.yaml',
];
@@ -3196,6 +3202,25 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
);
const name = 'ql3-plugin-package-executor';
const serviceAccount = namedResource(resources, 'ServiceAccount', name);
const actionName = 'ql3-plugin-package-secret-action';
const actionServiceAccount = namedResource(
resources,
'ServiceAccount',
actionName,
);
const admissionConfig = namedResource(resources, 'ConfigMap', actionName + '-admission');
const role = namedResource(resources, 'Role', name);
const roleBinding = namedResource(resources, 'RoleBinding', name);
const admissionPolicy = namedResource(
resources,
'ValidatingAdmissionPolicy',
actionName,
);
const admissionBinding = namedResource(
resources,
'ValidatingAdmissionPolicyBinding',
actionName,
);
const cronJob = namedResource(resources, 'CronJob', name);
const networkPolicy = namedResource(resources, 'NetworkPolicy', name);
const pod = objectAt(cronJob, [
@@ -3211,10 +3236,11 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
JSON.stringify(kustomization?.resources) !==
JSON.stringify(expectedResources) ||
serviceAccount?.automountServiceAccountToken !== false ||
actionServiceAccount?.automountServiceAccountToken !== false ||
cronJob?.spec?.schedule !== '*/2 * * * *' ||
cronJob?.spec?.concurrencyPolicy !== 'Forbid' ||
pod?.serviceAccountName !== name ||
pod?.automountServiceAccountToken !== false ||
pod?.automountServiceAccountToken !== true ||
pod?.restartPolicy !== 'Never' ||
pod?.securityContext?.runAsNonRoot !== true ||
pod?.securityContext?.seccompProfile?.type !== 'RuntimeDefault' ||
@@ -3227,7 +3253,71 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
findings.push(
finding(
'QL3_CLUSTER_PLUGIN_EXECUTOR_LIFECYCLE',
'Plugin Package executor must remain a bounded, tokenless, non-overlapping CronJob',
'Plugin Package executor must remain bounded and non-overlapping, with Kubernetes authority isolated from tokenless Secret action Jobs',
),
);
}
const expectedRoleRules = [
{
apiGroups: ['batch'],
resources: ['jobs'],
verbs: ['create', 'get'],
},
{
apiGroups: [''],
resources: ['configmaps'],
resourceNames: ['ql3-plugin-package-secret-action-admission'],
verbs: ['get'],
},
];
const admissionExpressions = (admissionPolicy?.spec?.validations ?? [])
.map((validation) => validation?.expression)
.join('\n');
if (
JSON.stringify(role?.rules) !== JSON.stringify(expectedRoleRules) ||
JSON.stringify(roleBinding?.subjects) !==
JSON.stringify([
{
kind: 'ServiceAccount',
name,
namespace: 'qinglong3-system',
},
]) ||
JSON.stringify(roleBinding?.roleRef) !==
JSON.stringify({
apiGroup: 'rbac.authorization.k8s.io',
kind: 'Role',
name,
}) ||
admissionPolicy?.spec?.failurePolicy !== 'Fail' ||
admissionPolicy?.spec?.paramKind?.apiVersion !== 'v1' ||
admissionPolicy?.spec?.paramKind?.kind !== 'ConfigMap' ||
admissionPolicy?.spec?.matchConstraints?.resourceRules?.[0]?.operations?.[0] !==
'CREATE' ||
admissionPolicy?.spec?.matchConstraints?.resourceRules?.[0]?.resources?.[0] !==
'jobs' ||
admissionPolicy?.spec?.matchConditions?.[0]?.expression !==
"request.userInfo.username == 'system:serviceaccount:qinglong3-system:ql3-plugin-package-executor'" ||
!admissionExpressions.includes("variables.executor.image == params.data.image") ||
!admissionExpressions.includes('variables.pod.automountServiceAccountToken == false') ||
!admissionExpressions.includes('variables.values.secret.items.all') ||
admissionBinding?.spec?.policyName !== actionName ||
admissionBinding?.spec?.paramRef?.name !== actionName + '-admission' ||
admissionBinding?.spec?.paramRef?.namespace !== 'qinglong3-system' ||
admissionBinding?.spec?.paramRef?.parameterNotFoundAction !== 'Deny' ||
JSON.stringify(admissionBinding?.spec?.validationActions) !==
JSON.stringify(['Deny']) ||
admissionConfig?.data?.serviceAccountName !== actionName ||
admissionConfig?.data?.sourceSecretName !==
'ql3-cluster-plugin-package-values' ||
!String(admissionConfig?.data?.image).endsWith(
'@sha256:0000000000000000000000000000000000000000000000000000000000000000',
)
) {
findings.push(
finding(
'QL3_CLUSTER_PLUGIN_EXECUTOR_KUBERNETES_AUTHORITY',
'Plugin Package controller must have create/get-only Job RBAC behind deny-on-error exact-contract admission and a tokenless action identity',
),
);
}
@@ -3254,6 +3344,9 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
['QL3_PLUGIN_PACKAGE_EXECUTOR_LEASE_DURATION_MS', '600000'],
['QL3_PLUGIN_PACKAGE_EXECUTOR_REVOCATION_PAGE_SIZE', '16'],
['QL3_PLUGIN_PACKAGE_EXECUTOR_REVOCATION_MAX_PAGES', '16'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED', 'true'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_LIMIT', '8'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_NAMESPACE', 'qinglong3-system'],
['QL3_POSTGRES_TLS_MODE', 'verify-full'],
[
'QL3_POSTGRES_TLS_CA_FILE',
@@ -3270,6 +3363,50 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
);
}
}
for (const [environmentName, key] of [
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_SERVICE_ACCOUNT', 'serviceAccountName'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_SOURCE_SECRET', 'sourceSecretName'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE', 'image'],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_SECRET',
'postgresCaSecretName',
],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_KEY', 'postgresCaKey'],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_SERVERNAME',
'postgresServerName',
],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET',
'postgresUrlSecretName',
],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY', 'postgresUrlSecretKey'],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_AUTH_SECRET',
'postgresAuthSecretName',
],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_HOST', 'postgresHost'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PORT', 'postgresPort'],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_DATABASE', 'postgresDatabase'],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_USERNAME_KEY',
'postgresUsernameKey',
],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PASSWORD_KEY',
'postgresPasswordKey',
],
]) {
const ref = env.get(environmentName)?.valueFrom?.configMapKeyRef;
if (ref?.name !== actionName + '-admission' || ref?.key !== key) {
findings.push(
finding(
'QL3_CLUSTER_PLUGIN_EXECUTOR_SECRET_ACTION_CONFIG',
`${environmentName} must come from the admission-bound ConfigMap`,
),
);
}
}
const executorUrl = env.get('QL3_POSTGRES_PACKAGE_EXECUTOR_URL')?.valueFrom
?.secretKeyRef;
if (
@@ -3348,6 +3485,38 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
),
);
}
const apiServerEgressExample = yaml.load(
readFile(
path.join(operationRoot, 'api-server-egress-patch.example.yaml'),
'utf8',
),
);
if (
JSON.stringify(apiServerEgressExample) !==
JSON.stringify([
{
op: 'add',
path: '/spec/egress/-',
value: {
to: [
{
ipBlock: {
cidr: 'REPLACE_WITH_API_SERVER_CIDR',
},
},
],
ports: [{ protocol: 'TCP', port: 443 }],
},
},
])
) {
findings.push(
finding(
'QL3_CLUSTER_PLUGIN_EXECUTOR_API_EGRESS',
'Plugin Package controller API egress example must require one exact private-overlay CIDR and TCP 443',
),
);
}
const cloudNativeRoot = path.join(operationRoot, 'cloudnative-pg');
const cloudNative = yaml.load(
readFile(path.join(cloudNativeRoot, 'kustomization.yaml'), 'utf8'),
@@ -3364,12 +3533,21 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
)
: undefined;
const cloudNativeEnv = environmentByName({ env: environmentPatch?.value });
const secretActionConfigPatch = yaml.load(
readFile(
path.join(cloudNativeRoot, 'secret-action-admission-config-patch.yaml'),
'utf8',
),
);
const secretActionConfig = secretActionConfigPatch?.[0]?.value;
const unreleasedDigest =
'sha256:0000000000000000000000000000000000000000000000000000000000000000';
if (
JSON.stringify(cloudNative?.resources) !== JSON.stringify(['../base']) ||
cloudNative?.patches?.[0]?.path !== 'cron-job-patch.yaml' ||
cloudNative?.patches?.[1]?.path !== 'network-policy-patch.yaml' ||
cloudNative?.patches?.[2]?.path !==
'secret-action-admission-config-patch.yaml' ||
JSON.stringify(cloudNative?.images) !==
JSON.stringify([
{
@@ -3387,6 +3565,19 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
?.secretKeyRef?.name !== 'ql3-postgres-package-executor-auth' ||
cloudNativeEnv.get('QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD')?.valueFrom
?.secretKeyRef?.name !== 'ql3-postgres-package-executor-auth' ||
secretActionConfigPatch?.[0]?.op !== 'replace' ||
secretActionConfigPatch?.[0]?.path !== '/data' ||
secretActionConfig?.postgresConnectionMode !== 'fields' ||
secretActionConfig?.postgresCaSecretName !== 'ql3-postgres-ca' ||
secretActionConfig?.postgresServerName !==
'ql3-postgres-rw.qinglong3-system.svc' ||
secretActionConfig?.postgresAuthSecretName !==
'ql3-postgres-package-executor-auth' ||
secretActionConfig?.postgresHost !==
'ql3-postgres-rw.qinglong3-system.svc' ||
secretActionConfig?.postgresPort !== '5432' ||
secretActionConfig?.postgresDatabase !== 'qinglong' ||
!String(secretActionConfig?.image).endsWith(`@${unreleasedDigest}`) ||
!patch.some(
(operation) =>
operation?.path ===
@@ -64,6 +64,56 @@ test('requires every Cluster Admin Kubernetes workload to override the image com
);
});
test('rejects widened Plugin Package Secret action Kubernetes authority', () => {
const widenedRole = auditClusterDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/plugin-package-executor/base/role.yaml',
(source) => source.replace(' - get\n', ' - get\n - list\n'),
),
});
assert.equal(widenedRole.compatible, false);
assert.equal(
widenedRole.findings.some(
({ code }) => code === 'QL3_CLUSTER_PLUGIN_EXECUTOR_KUBERNETES_AUTHORITY',
),
true,
);
const ignoredAdmissionFailure = auditClusterDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/plugin-package-executor/base/validating-admission-policy.yaml',
(source) => source.replace('failurePolicy: Fail', 'failurePolicy: Ignore'),
),
});
assert.equal(ignoredAdmissionFailure.compatible, false);
assert.equal(
ignoredAdmissionFailure.findings.some(
({ code }) => code === 'QL3_CLUSTER_PLUGIN_EXECUTOR_KUBERNETES_AUTHORITY',
),
true,
);
const actionToken = auditClusterDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/plugin-package-executor/base/secret-action-service-account.yaml',
(source) => source.replace(
'automountServiceAccountToken: false',
'automountServiceAccountToken: true',
),
),
});
assert.equal(actionToken.compatible, false);
assert.equal(
actionToken.findings.some(
({ code }) => code === 'QL3_CLUSTER_PLUGIN_EXECUTOR_LIFECYCLE',
),
true,
);
});
test('requires the bounded Cluster product facade and image entrypoint', () => {
const missingBinary = auditClusterDeployment({
root: ROOT,
+2 -2
View File
@@ -340,10 +340,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: clusterAdmin.rootSourceFileRoles,
},
{
sourceFiles: 108,
sourceFiles: 109,
rootSourceFiles: 1,
rootSourceLines: 61,
nestedSourceFiles: 107,
nestedSourceFiles: 108,
rootSourceFileRoles: {
'modelInvocationMigrationCli.ts': 'binary_entry',
},