diff --git a/.gitea/workflows/quality-gates.yml b/.gitea/workflows/quality-gates.yml new file mode 100644 index 0000000..fdc8d3a --- /dev/null +++ b/.gitea/workflows/quality-gates.yml @@ -0,0 +1,35 @@ +name: quality-gates + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + - name: Install project and pinned quality tools + shell: bash + run: | + set -euo pipefail + python -m pip install --upgrade pip + if [[ -f requirements.txt ]]; then python -m pip install -r requirements.txt; fi + if [[ -f requirements-dev.txt ]]; then python -m pip install -r requirements-dev.txt; fi + python -m pip install ruff==0.6.9 mypy==1.11.2 pytest==8.3.3 + - name: Lint + run: python -m ruff check . + - name: Type check + run: python -m mypy app + - name: Test + run: python -m pytest -q diff --git a/docs/operator-runbook.md b/docs/operator-runbook.md new file mode 100644 index 0000000..65c92f0 --- /dev/null +++ b/docs/operator-runbook.md @@ -0,0 +1,20 @@ +# Content Ingestion Agent operator runbook + +## Deploying + +Apply `k8s/configmap.yaml`, `k8s/deployment.yaml`, and `k8s/service.yaml` in that order. The deployment intentionally does **not** contain credentials. Before applying it, provision a Kubernetes Secret named `content-ingestion-agent-secrets` through the cluster's approved secret manager or External Secrets controller. The Secret must provide `github-token` and `sharepoint-client-secret`; do not commit a Secret manifest, token, certificate, or client secret to this repository. + +Verify the resulting pod with `kubectl -n get pods -l app.kubernetes.io/name=content-ingestion-agent` and inspect only non-sensitive status and logs. Rotate credentials in the external secret manager, then restart the deployment to pick up the rotated values. + +## Capability and approval boundary + +The agent can read configured GitHub repositories and SharePoint content. Its publication connector can perform the world-changing action of writing/publishing content to the configured CMS. Publication is disabled by default by `PUBLICATION_ENABLED=false` and remains approval-gated by `REQUIRE_PUBLICATION_APPROVAL=true`. Operators must review the proposed changes and explicitly approve a publication request; never grant the runtime identity broader repository or CMS permissions than required. + +If publication is enabled for an environment, record the approver, target, change summary, and rollback procedure before deployment. Test ingestion against a non-production source first. To stop publication immediately, set `PUBLICATION_ENABLED=false` in the ConfigMap and roll out the deployment. + +## Secret-handling checks + +- Credentials are consumed only through `secretRef`/`secretKeyRef` environment bindings. +- ConfigMap data is limited to non-sensitive runtime flags and provider names. +- Do not use `kubectl get secret ... -o yaml` in support output, and do not place secret values in issue reports or logs. +- The workflow and manifests contain no literal credential values. diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index c6f2612..22dd0a0 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -1,8 +1,12 @@ apiVersion: v1 kind: ConfigMap metadata: - name: content-ingestion-agent + name: content-ingestion-agent-config + labels: + app.kubernetes.io/name: content-ingestion-agent data: - ENVIRONMENT: production - REQUIRE_APPROVAL: "true" - SHAREPOINT_SITE_URL: https://sharepoint.example.com + LOG_LEVEL: INFO + PUBLICATION_ENABLED: "false" + REQUIRE_PUBLICATION_APPROVAL: "true" + SCM_PROVIDER: github + CMS_PROVIDER: sharepoint diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index a4bd401..7b16c54 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -2,16 +2,67 @@ apiVersion: apps/v1 kind: Deployment metadata: name: content-ingestion-agent + labels: + app.kubernetes.io/name: content-ingestion-agent spec: replicas: 1 selector: - matchLabels: {app: content-ingestion-agent} + matchLabels: + app.kubernetes.io/name: content-ingestion-agent template: - metadata: {labels: {app: content-ingestion-agent}} + metadata: + labels: + app.kubernetes.io/name: content-ingestion-agent spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault containers: - name: agent image: content-ingestion-agent:latest - ports: [{containerPort: 8080}] - envFrom: [{configMapRef: {name: content-ingestion-agent}}] - readinessProbe: {httpGet: {path: /health, port: 8080}} + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + envFrom: + - configMapRef: + name: content-ingestion-agent-config + - secretRef: + name: content-ingestion-agent-secrets + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: content-ingestion-agent-secrets + key: github-token + - name: SHAREPOINT_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: content-ingestion-agent-secrets + key: sharepoint-client-secret + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] diff --git a/k8s/service.yaml b/k8s/service.yaml index 7d3434a..0771d12 100644 --- a/k8s/service.yaml +++ b/k8s/service.yaml @@ -2,9 +2,13 @@ apiVersion: v1 kind: Service metadata: name: content-ingestion-agent + labels: + app.kubernetes.io/name: content-ingestion-agent spec: - selector: {app: content-ingestion-agent} + type: ClusterIP + selector: + app.kubernetes.io/name: content-ingestion-agent ports: - name: http port: 80 - targetPort: 8080 + targetPort: http