CD/CD consolidation
This commit is contained in:
47
devops/services/advisory-ai/Dockerfile
Normal file
47
devops/services/advisory-ai/Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
|
||||
# StellaOps AdvisoryAI – multi-role container build
|
||||
# Build arg PROJECT selects WebService or Worker; defaults to WebService.
|
||||
# Example builds:
|
||||
# docker build -f ops/advisory-ai/Dockerfile -t stellaops-advisoryai-web \
|
||||
# --build-arg PROJECT=src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/StellaOps.AdvisoryAI.WebService.csproj \
|
||||
# --build-arg APP_DLL=StellaOps.AdvisoryAI.WebService.dll .
|
||||
# docker build -f ops/advisory-ai/Dockerfile -t stellaops-advisoryai-worker \
|
||||
# --build-arg PROJECT=src/AdvisoryAI/StellaOps.AdvisoryAI.Worker/StellaOps.AdvisoryAI.Worker.csproj \
|
||||
# --build-arg APP_DLL=StellaOps.AdvisoryAI.Worker.dll .
|
||||
|
||||
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/nightly/sdk:10.0
|
||||
ARG RUNTIME_IMAGE=gcr.io/distroless/dotnet/aspnet:latest
|
||||
ARG PROJECT=src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/StellaOps.AdvisoryAI.WebService.csproj
|
||||
ARG APP_DLL=StellaOps.AdvisoryAI.WebService.dll
|
||||
|
||||
FROM ${SDK_IMAGE} AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY . .
|
||||
|
||||
# Restore only AdvisoryAI graph to keep build smaller.
|
||||
RUN dotnet restore ${PROJECT}
|
||||
|
||||
RUN dotnet publish ${PROJECT} \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM ${RUNTIME_IMAGE} AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \
|
||||
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true \
|
||||
ADVISORYAI__STORAGE__PLANCACHEDIRECTORY=/app/data/plans \
|
||||
ADVISORYAI__STORAGE__OUTPUTDIRECTORY=/app/data/outputs \
|
||||
ADVISORYAI__QUEUE__DIRECTORYPATH=/app/data/queue \
|
||||
ADVISORYAI__INFERENCE__MODE=Local
|
||||
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Writable mount for queue/cache/output. Guardrail/guardrails can also be mounted under /app/etc.
|
||||
VOLUME ["/app/data", "/app/etc"]
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "${APP_DLL}"]
|
||||
47
devops/services/advisory-ai/README.md
Normal file
47
devops/services/advisory-ai/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# AdvisoryAI packaging (AIAI-31-008)
|
||||
|
||||
Artifacts delivered for on-prem / air-gapped deployment:
|
||||
|
||||
- `ops/advisory-ai/Dockerfile` builds WebService and Worker images (multi-role via `PROJECT`/`APP_DLL` args).
|
||||
- `ops/advisory-ai/docker-compose.advisoryai.yaml` runs WebService + Worker with shared data volume; ships remote inference toggle envs.
|
||||
- `ops/advisory-ai/helm/` provides a minimal chart (web + worker) with storage mounts, optional PVC, and remote inference settings.
|
||||
|
||||
## Build images
|
||||
```bash
|
||||
# WebService
|
||||
docker build -f ops/advisory-ai/Dockerfile \
|
||||
-t stellaops-advisoryai-web:dev \
|
||||
--build-arg PROJECT=src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/StellaOps.AdvisoryAI.WebService.csproj \
|
||||
--build-arg APP_DLL=StellaOps.AdvisoryAI.WebService.dll .
|
||||
|
||||
# Worker
|
||||
docker build -f ops/advisory-ai/Dockerfile \
|
||||
-t stellaops-advisoryai-worker:dev \
|
||||
--build-arg PROJECT=src/AdvisoryAI/StellaOps.AdvisoryAI.Worker/StellaOps.AdvisoryAI.Worker.csproj \
|
||||
--build-arg APP_DLL=StellaOps.AdvisoryAI.Worker.dll .
|
||||
```
|
||||
|
||||
## Local/offline compose
|
||||
```bash
|
||||
cd ops/advisory-ai
|
||||
docker compose -f docker-compose.advisoryai.yaml up -d --build
|
||||
```
|
||||
- Set `ADVISORYAI__INFERENCE__MODE=Remote` plus `ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS`/`APIKEY` to offload inference.
|
||||
- Default mode is Local (offline-friendly). Queue/cache/output live under `/app/data` (binds to `advisoryai-data` volume).
|
||||
|
||||
## Helm (cluster)
|
||||
```bash
|
||||
helm upgrade --install advisoryai ops/advisory-ai/helm \
|
||||
--set image.repository=stellaops-advisoryai-web \
|
||||
--set image.tag=dev \
|
||||
--set inference.mode=Local
|
||||
```
|
||||
- Enable remote inference: `--set inference.mode=Remote --set inference.remote.baseAddress=https://inference.your.domain --set inference.remote.apiKey=<token>`.
|
||||
- Enable persistence: `--set storage.persistence.enabled=true --set storage.persistence.size=10Gi` or `--set storage.persistence.existingClaim=<pvc>`.
|
||||
- Worker replicas: `--set worker.replicas=2` (or `--set worker.enabled=false` to run WebService only).
|
||||
|
||||
## Operational notes
|
||||
- Data paths (`/app/data/plans`, `/app/data/queue`, `/app/data/outputs`) are configurable via env and pre-created at startup.
|
||||
- Guardrail phrases or policy knobs can be mounted under `/app/etc`; point `ADVISORYAI__GUARDRAILS__PHRASESLIST` to the mounted file.
|
||||
- Observability follows standard ASP.NET JSON logs; add OTEL exporters via `OTEL_EXPORTER_OTLP_ENDPOINT` env when allowed. Keep disabled in sealed/offline deployments.
|
||||
- For air-gapped clusters, publish built images to your registry and reference via `--set image.repository=<registry>/stellaops/advisoryai-web`.
|
||||
55
devops/services/advisory-ai/docker-compose.advisoryai.yaml
Normal file
55
devops/services/advisory-ai/docker-compose.advisoryai.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
version: "3.9"
|
||||
|
||||
# Local/offline deployment for AdvisoryAI WebService + Worker.
|
||||
services:
|
||||
advisoryai-web:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: ops/advisory-ai/Dockerfile
|
||||
args:
|
||||
PROJECT: src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/StellaOps.AdvisoryAI.WebService.csproj
|
||||
APP_DLL: StellaOps.AdvisoryAI.WebService.dll
|
||||
image: stellaops-advisoryai-web:dev
|
||||
depends_on:
|
||||
- advisoryai-worker
|
||||
environment:
|
||||
ASPNETCORE_URLS: "http://0.0.0.0:8080"
|
||||
ADVISORYAI__QUEUE__DIRECTORYPATH: "/app/data/queue"
|
||||
ADVISORYAI__STORAGE__PLANCACHEDIRECTORY: "/app/data/plans"
|
||||
ADVISORYAI__STORAGE__OUTPUTDIRECTORY: "/app/data/outputs"
|
||||
ADVISORYAI__INFERENCE__MODE: "Local" # switch to Remote to call an external inference host
|
||||
# ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS: "https://inference.example.com"
|
||||
# ADVISORYAI__INFERENCE__REMOTE__ENDPOINT: "/v1/inference"
|
||||
# ADVISORYAI__INFERENCE__REMOTE__APIKEY: "set-me"
|
||||
# ADVISORYAI__INFERENCE__REMOTE__TIMEOUT: "00:00:30"
|
||||
# Example SBOM context feed; optional.
|
||||
# ADVISORYAI__SBOMBASEADDRESS: "https://sbom.local/v1/sbom/context"
|
||||
# ADVISORYAI__SBOMTENANT: "tenant-a"
|
||||
# ADVISORYAI__GUARDRAILS__PHRASESLIST: "/app/etc/guardrails/phrases.txt"
|
||||
volumes:
|
||||
- advisoryai-data:/app/data
|
||||
- ./etc:/app/etc:ro
|
||||
ports:
|
||||
- "7071:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
advisoryai-worker:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: ops/advisory-ai/Dockerfile
|
||||
args:
|
||||
PROJECT: src/AdvisoryAI/StellaOps.AdvisoryAI.Worker/StellaOps.AdvisoryAI.Worker.csproj
|
||||
APP_DLL: StellaOps.AdvisoryAI.Worker.dll
|
||||
image: stellaops-advisoryai-worker:dev
|
||||
environment:
|
||||
ADVISORYAI__QUEUE__DIRECTORYPATH: "/app/data/queue"
|
||||
ADVISORYAI__STORAGE__PLANCACHEDIRECTORY: "/app/data/plans"
|
||||
ADVISORYAI__STORAGE__OUTPUTDIRECTORY: "/app/data/outputs"
|
||||
ADVISORYAI__INFERENCE__MODE: "Local"
|
||||
volumes:
|
||||
- advisoryai-data:/app/data
|
||||
- ./etc:/app/etc:ro
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
advisoryai-data:
|
||||
0
devops/services/advisory-ai/etc/.gitkeep
Normal file
0
devops/services/advisory-ai/etc/.gitkeep
Normal file
6
devops/services/advisory-ai/helm/Chart.yaml
Normal file
6
devops/services/advisory-ai/helm/Chart.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: stellaops-advisoryai
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
description: AdvisoryAI WebService + Worker packaging for on-prem/air-gapped installs.
|
||||
type: application
|
||||
12
devops/services/advisory-ai/helm/templates/_helpers.tpl
Normal file
12
devops/services/advisory-ai/helm/templates/_helpers.tpl
Normal file
@@ -0,0 +1,12 @@
|
||||
{{- define "stellaops-advisoryai.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "stellaops-advisoryai.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
71
devops/services/advisory-ai/helm/templates/deployment.yaml
Normal file
71
devops/services/advisory-ai/helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "stellaops-advisoryai.fullname" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://0.0.0.0:{{ .Values.service.port }}"
|
||||
- name: ADVISORYAI__INFERENCE__MODE
|
||||
value: "{{ .Values.inference.mode }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS
|
||||
value: "{{ .Values.inference.remote.baseAddress }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__ENDPOINT
|
||||
value: "{{ .Values.inference.remote.endpoint }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__APIKEY
|
||||
value: "{{ .Values.inference.remote.apiKey }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__TIMEOUT
|
||||
value: "{{ printf "00:00:%d" .Values.inference.remote.timeoutSeconds }}"
|
||||
- name: ADVISORYAI__STORAGE__PLANCACHEDIRECTORY
|
||||
value: {{ .Values.storage.planCachePath | quote }}
|
||||
- name: ADVISORYAI__STORAGE__OUTPUTDIRECTORY
|
||||
value: {{ .Values.storage.outputPath | quote }}
|
||||
- name: ADVISORYAI__QUEUE__DIRECTORYPATH
|
||||
value: {{ .Values.storage.queuePath | quote }}
|
||||
envFrom:
|
||||
{{- if .Values.extraEnvFrom }}
|
||||
- secretRef:
|
||||
name: {{ .Values.extraEnvFrom | first }}
|
||||
{{- end }}
|
||||
{{- if .Values.extraEnv }}
|
||||
{{- range .Values.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.service.port }}
|
||||
volumeMounts:
|
||||
- name: advisoryai-data
|
||||
mountPath: /app/data
|
||||
resources: {{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: advisoryai-data
|
||||
{{- if .Values.storage.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.storage.persistence.existingClaim | default (printf "%s-data" (include "stellaops-advisoryai.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
nodeSelector: {{- toYaml .Values.nodeSelector | nindent 8 }}
|
||||
tolerations: {{- toYaml .Values.tolerations | nindent 8 }}
|
||||
affinity: {{- toYaml .Values.affinity | nindent 8 }}
|
||||
15
devops/services/advisory-ai/helm/templates/pvc.yaml
Normal file
15
devops/services/advisory-ai/helm/templates/pvc.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
{{- if and .Values.storage.persistence.enabled (not .Values.storage.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ printf "%s-data" (include "stellaops-advisoryai.fullname" .) }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.storage.persistence.size }}
|
||||
{{- end }}
|
||||
17
devops/services/advisory-ai/helm/templates/service.yaml
Normal file
17
devops/services/advisory-ai/helm/templates/service.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "stellaops-advisoryai.fullname" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
66
devops/services/advisory-ai/helm/templates/worker.yaml
Normal file
66
devops/services/advisory-ai/helm/templates/worker.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
{{- if .Values.worker.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "stellaops-advisoryai.fullname" . }}-worker
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}-worker
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
replicas: {{ .Values.worker.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}-worker
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stellaops-advisoryai.name" . }}-worker
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
containers:
|
||||
- name: worker
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command: ["dotnet", "StellaOps.AdvisoryAI.Worker.dll"]
|
||||
env:
|
||||
- name: ADVISORYAI__INFERENCE__MODE
|
||||
value: "{{ .Values.inference.mode }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS
|
||||
value: "{{ .Values.inference.remote.baseAddress }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__ENDPOINT
|
||||
value: "{{ .Values.inference.remote.endpoint }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__APIKEY
|
||||
value: "{{ .Values.inference.remote.apiKey }}"
|
||||
- name: ADVISORYAI__INFERENCE__REMOTE__TIMEOUT
|
||||
value: "{{ printf "00:00:%d" .Values.inference.remote.timeoutSeconds }}"
|
||||
- name: ADVISORYAI__STORAGE__PLANCACHEDIRECTORY
|
||||
value: {{ .Values.storage.planCachePath | quote }}
|
||||
- name: ADVISORYAI__STORAGE__OUTPUTDIRECTORY
|
||||
value: {{ .Values.storage.outputPath | quote }}
|
||||
- name: ADVISORYAI__QUEUE__DIRECTORYPATH
|
||||
value: {{ .Values.storage.queuePath | quote }}
|
||||
envFrom:
|
||||
{{- if .Values.extraEnvFrom }}
|
||||
- secretRef:
|
||||
name: {{ .Values.extraEnvFrom | first }}
|
||||
{{- end }}
|
||||
{{- if .Values.extraEnv }}
|
||||
{{- range .Values.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: advisoryai-data
|
||||
mountPath: /app/data
|
||||
resources: {{- toYaml .Values.worker.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: advisoryai-data
|
||||
{{- if .Values.storage.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.storage.persistence.existingClaim | default (printf "%s-data" (include "stellaops-advisoryai.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
38
devops/services/advisory-ai/helm/values.yaml
Normal file
38
devops/services/advisory-ai/helm/values.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
image:
|
||||
repository: stellaops/advisoryai
|
||||
tag: dev
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
port: 8080
|
||||
type: ClusterIP
|
||||
|
||||
inference:
|
||||
mode: Local # or Remote
|
||||
remote:
|
||||
baseAddress: ""
|
||||
endpoint: "/v1/inference"
|
||||
apiKey: ""
|
||||
timeoutSeconds: 30
|
||||
|
||||
storage:
|
||||
planCachePath: /app/data/plans
|
||||
outputPath: /app/data/outputs
|
||||
queuePath: /app/data/queue
|
||||
persistence:
|
||||
enabled: false
|
||||
existingClaim: ""
|
||||
size: 5Gi
|
||||
|
||||
resources: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
worker:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
resources: {}
|
||||
|
||||
extraEnv: [] # list of { name: ..., value: ... }
|
||||
extraEnvFrom: []
|
||||
0
devops/services/advisoryai-ci-runner/.gitkeep
Normal file
0
devops/services/advisoryai-ci-runner/.gitkeep
Normal file
25
devops/services/advisoryai-ci-runner/README.md
Normal file
25
devops/services/advisoryai-ci-runner/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Advisory AI CI Runner Harness (DEVOPS-AIAI-31-001)
|
||||
|
||||
Purpose: deterministic, offline-friendly CI harness for Advisory AI service/worker. Produces warmed-cache restore, build binlog, and TRX outputs for the core test suite so downstream sprints can validate without bespoke pipelines.
|
||||
|
||||
Usage
|
||||
- From repo root run: `ops/devops/advisoryai-ci-runner/run-advisoryai-ci.sh`
|
||||
- Outputs land in `ops/devops/artifacts/advisoryai-ci/<UTC timestamp>/`:
|
||||
- `build.binlog` (solution build)
|
||||
- `tests/advisoryai.trx` (VSTest results)
|
||||
- `summary.json` (paths + hashes + durations)
|
||||
|
||||
Environment
|
||||
- Defaults: `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1`, `NUGET_PACKAGES=$REPO/.nuget/packages`.
|
||||
- Sources default to `local-nugets` then the warmed cache; override via `NUGET_SOURCES` (semicolon-separated).
|
||||
- No external services required; tests are isolated/local.
|
||||
|
||||
What it does
|
||||
1) Warm NuGet cache from `local-nugets/` into `$NUGET_PACKAGES` for air-gap parity.
|
||||
2) `dotnet restore` + `dotnet build` on `src/AdvisoryAI/StellaOps.AdvisoryAI.sln` with `/bl`.
|
||||
3) Run the AdvisoryAI test project (`__Tests/StellaOps.AdvisoryAI.Tests`) with TRX output; optional `TEST_FILTER` env narrows scope.
|
||||
4) Emit `summary.json` with artefact paths and SHA256s for reproducibility.
|
||||
|
||||
Notes
|
||||
- Timestamped output folders keep ordering deterministic; consumers should sort lexicographically.
|
||||
- Use `TEST_FILTER="Name~Inference"` to target inference/monitoring-specific tests when iterating.
|
||||
67
devops/services/advisoryai-ci-runner/run-advisoryai-ci.sh
Normal file
67
devops/services/advisoryai-ci-runner/run-advisoryai-ci.sh
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Advisory AI CI runner (DEVOPS-AIAI-31-001)
|
||||
# Builds solution and runs tests with warmed NuGet cache; emits binlog + TRX summary.
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
out_dir="$repo_root/ops/devops/artifacts/advisoryai-ci/$ts"
|
||||
logs_dir="$out_dir/tests"
|
||||
mkdir -p "$logs_dir"
|
||||
|
||||
# Deterministic env
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT:-1}
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=${DOTNET_SKIP_FIRST_TIME_EXPERIENCE:-1}
|
||||
export NUGET_PACKAGES=${NUGET_PACKAGES:-$repo_root/.nuget/packages}
|
||||
export NUGET_SOURCES=${NUGET_SOURCES:-"$repo_root/local-nugets;$repo_root/.nuget/packages"}
|
||||
export TEST_FILTER=${TEST_FILTER:-""}
|
||||
export DOTNET_RESTORE_DISABLE_PARALLEL=${DOTNET_RESTORE_DISABLE_PARALLEL:-1}
|
||||
|
||||
# Warm cache from local feed
|
||||
mkdir -p "$NUGET_PACKAGES"
|
||||
rsync -a "$repo_root/local-nugets/" "$NUGET_PACKAGES/" >/dev/null 2>&1 || true
|
||||
|
||||
# Restore sources
|
||||
restore_sources=()
|
||||
IFS=';' read -ra SRC_ARR <<< "$NUGET_SOURCES"
|
||||
for s in "${SRC_ARR[@]}"; do
|
||||
[[ -n "$s" ]] && restore_sources+=(--source "$s")
|
||||
done
|
||||
|
||||
solution="$repo_root/src/AdvisoryAI/StellaOps.AdvisoryAI.sln"
|
||||
dotnet restore "$solution" --ignore-failed-sources "${restore_sources[@]}"
|
||||
|
||||
# Build with binlog (Release for perf parity)
|
||||
build_binlog="$out_dir/build.binlog"
|
||||
dotnet build "$solution" -c Release /p:ContinuousIntegrationBuild=true /bl:"$build_binlog"
|
||||
|
||||
# Tests
|
||||
common_test_args=( -c Release --no-build --results-directory "$logs_dir" )
|
||||
if [[ -n "$TEST_FILTER" ]]; then
|
||||
common_test_args+=( --filter "$TEST_FILTER" )
|
||||
fi
|
||||
|
||||
trx_name="advisoryai.trx"
|
||||
dotnet test "$repo_root/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj" \
|
||||
"${common_test_args[@]}" \
|
||||
--logger "trx;LogFileName=$trx_name"
|
||||
|
||||
# Summarize artefacts
|
||||
summary="$out_dir/summary.json"
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "timestamp_utc": "%s",\n' "$ts"
|
||||
printf ' "build_binlog": "%s",\n' "${build_binlog#${repo_root}/}"
|
||||
printf ' "tests": [{"project":"AdvisoryAI","trx":"%s"}],\n' "${logs_dir#${repo_root}/}/$trx_name"
|
||||
printf ' "nuget_packages": "%s",\n' "${NUGET_PACKAGES#${repo_root}/}"
|
||||
printf ' "sources": [\n'
|
||||
for i in "${!SRC_ARR[@]}"; do
|
||||
sep=","; [[ $i -eq $((${#SRC_ARR[@]}-1)) ]] && sep=""
|
||||
printf ' "%s"%s\n' "${SRC_ARR[$i]}" "$sep"
|
||||
done
|
||||
printf ' ]\n'
|
||||
printf '}\n'
|
||||
} > "$summary"
|
||||
|
||||
echo "Artifacts written to ${out_dir#${repo_root}/}"
|
||||
25
devops/services/aoc/aoc-ci.md
Normal file
25
devops/services/aoc/aoc-ci.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# AOC Analyzer CI Contract (DEVOPS-AOC-19-001)
|
||||
|
||||
## Scope
|
||||
Integrate AOC Roslyn analyzer and guard tests into CI to block banned writes in ingestion projects.
|
||||
|
||||
## Steps
|
||||
1) Restore & build analyzers
|
||||
- `dotnet restore src/Aoc/__Analyzers/StellaOps.Aoc.Analyzers/StellaOps.Aoc.Analyzers.csproj`
|
||||
- `dotnet build src/Aoc/__Analyzers/StellaOps.Aoc.Analyzers/StellaOps.Aoc.Analyzers.csproj -c Release`
|
||||
2) Run analyzer on ingestion projects (Authority/Concelier/Excititor ingest paths)
|
||||
- `dotnet build src/Concelier/StellaOps.Concelier.Ingestion/StellaOps.Concelier.Ingestion.csproj -c Release /p:RunAnalyzers=true /p:TreatWarningsAsErrors=true`
|
||||
- `dotnet build src/Authority/StellaOps.Authority.Ingestion/StellaOps.Authority.Ingestion.csproj -c Release /p:RunAnalyzers=true /p:TreatWarningsAsErrors=true`
|
||||
- `dotnet build src/Excititor/StellaOps.Excititor.Ingestion/StellaOps.Excititor.Ingestion.csproj -c Release /p:RunAnalyzers=true /p:TreatWarningsAsErrors=true`
|
||||
3) Guard tests
|
||||
- `dotnet test src/Aoc/__Tests/StellaOps.Aoc.Analyzers.Tests/StellaOps.Aoc.Analyzers.Tests.csproj -c Release`
|
||||
4) Artefacts
|
||||
- Upload `.artifacts/aoc-analyzer.log` and test TRX.
|
||||
|
||||
## Determinism/Offline
|
||||
- Use local feeds (`local-nugets/`); no external fetches post-restore.
|
||||
- Build with `/p:ContinuousIntegrationBuild=true`.
|
||||
|
||||
## Acceptance
|
||||
- CI fails on any analyzer warning in ingestion projects.
|
||||
- Tests pass; artefacts uploaded.
|
||||
22
devops/services/aoc/aoc-verify-stage.md
Normal file
22
devops/services/aoc/aoc-verify-stage.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# AOC Verify Stage (DEVOPS-AOC-19-002)
|
||||
|
||||
## Purpose
|
||||
Add CI stage to run `stella aoc verify --since <commit>` against seeded Mongo snapshots for Concelier + Excititor, publishing violation reports.
|
||||
|
||||
## Inputs
|
||||
- `STAGING_MONGO_URI` (read-only snapshot).
|
||||
- Optional `AOC_VERIFY_SINCE` (defaults to `HEAD~1`).
|
||||
|
||||
## Steps
|
||||
1) Seed snapshot (if needed)
|
||||
- Restore snapshot into local Mongo or point to read-only staging snapshot.
|
||||
2) Run verify
|
||||
- `dotnet run --project src/Aoc/StellaOps.Aoc.Cli -- verify --since ${AOC_VERIFY_SINCE:-HEAD~1} --mongo $STAGING_MONGO_URI --output .artifacts/aoc-verify.json`
|
||||
3) Fail on violations
|
||||
- Parse `.artifacts/aoc-verify.json`; if `violations > 0`, fail with summary.
|
||||
4) Publish artifacts
|
||||
- Upload `.artifacts/aoc-verify.json` and `.artifacts/aoc-verify.ndjson` (per-violation).
|
||||
|
||||
## Acceptance
|
||||
- Stage fails when violations exist; passes clean otherwise.
|
||||
- Artifacts attached for auditing.
|
||||
73
devops/services/aoc/backfill-release-plan.md
Normal file
73
devops/services/aoc/backfill-release-plan.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# AOC Backfill Release Plan (DEVOPS-STORE-AOC-19-005-REL)
|
||||
|
||||
Scope: Release/offline-kit packaging for Concelier AOC backfill operations.
|
||||
|
||||
## Prerequisites
|
||||
- Dataset hash from dev rehearsal (AOC-19-005 dev outputs)
|
||||
- AOC guard tests passing (DEVOPS-AOC-19-001/002/003 - DONE)
|
||||
- Supersedes rollout plan reviewed (ops/devops/aoc/supersedes-rollout.md)
|
||||
|
||||
## Artefacts
|
||||
- Backfill runner bundle:
|
||||
- `aoc-backfill-runner.tar.gz` - CLI tool + scripts
|
||||
- `aoc-backfill-runner.sbom.json` - SPDX SBOM
|
||||
- `aoc-backfill-runner.dsse.json` - Cosign attestation
|
||||
- Dataset bundle:
|
||||
- `aoc-dataset-{hash}.tar.gz` - Seeded dataset
|
||||
- `aoc-dataset-{hash}.manifest.json` - Manifest with checksums
|
||||
- `aoc-dataset-{hash}.provenance.json` - SLSA provenance
|
||||
- Offline kit slice:
|
||||
- All above + SHA256SUMS + verification scripts
|
||||
|
||||
## Packaging Script
|
||||
|
||||
```bash
|
||||
# Production (CI with secrets)
|
||||
./ops/devops/aoc/package-backfill-release.sh
|
||||
|
||||
# Development (dev key)
|
||||
COSIGN_ALLOW_DEV_KEY=1 COSIGN_PASSWORD=stellaops-dev \
|
||||
DATASET_HASH=dev-rehearsal-placeholder \
|
||||
./ops/devops/aoc/package-backfill-release.sh
|
||||
```
|
||||
|
||||
## Pipeline Outline
|
||||
1) Build backfill runner from `src/Aoc/StellaOps.Aoc.Cli/`
|
||||
2) Generate SBOM with syft
|
||||
3) Sign with cosign (dev key fallback)
|
||||
4) Package dataset (when hash available)
|
||||
5) Create offline bundle with checksums
|
||||
6) Verification:
|
||||
- `stella aoc verify --dry-run`
|
||||
- `cosign verify-blob` for all bundles
|
||||
- `sha256sum --check`
|
||||
7) Publish to release bucket + offline kit
|
||||
|
||||
## Runbook
|
||||
1) Validate AOC guard tests pass in CI
|
||||
2) Run dev rehearsal with test dataset
|
||||
3) Capture dataset hash from rehearsal
|
||||
4) Execute packaging script with production key
|
||||
5) Verify all signatures and checksums
|
||||
6) Upload to release bucket
|
||||
7) Include in offline kit manifest
|
||||
|
||||
## CI Workflow
|
||||
`.gitea/workflows/aoc-backfill-release.yml`
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
# Verify bundle signatures
|
||||
cosign verify-blob \
|
||||
--key tools/cosign/cosign.dev.pub \
|
||||
--bundle out/aoc/aoc-backfill-runner.dsse.json \
|
||||
out/aoc/aoc-backfill-runner.tar.gz
|
||||
|
||||
# Verify checksums
|
||||
cd out/aoc && sha256sum -c SHA256SUMS
|
||||
```
|
||||
|
||||
## Owners
|
||||
- DevOps Guild (pipeline + packaging)
|
||||
- Concelier Storage Guild (dataset + backfill logic)
|
||||
- Platform Security (signing policy)
|
||||
175
devops/services/aoc/package-backfill-release.sh
Normal file
175
devops/services/aoc/package-backfill-release.sh
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env bash
|
||||
# Package AOC backfill release for offline kit
|
||||
# Usage: ./package-backfill-release.sh
|
||||
# Dev mode: COSIGN_ALLOW_DEV_KEY=1 COSIGN_PASSWORD=stellaops-dev DATASET_HASH=dev ./package-backfill-release.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
|
||||
OUT_DIR="${OUT_DIR:-$ROOT/out/aoc}"
|
||||
CREATED="${CREATED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
|
||||
DATASET_HASH="${DATASET_HASH:-}"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
echo "==> AOC Backfill Release Packaging"
|
||||
echo " Output: $OUT_DIR"
|
||||
echo " Dataset hash: ${DATASET_HASH:-<pending>}"
|
||||
|
||||
# Key resolution (same pattern as advisory-ai packaging)
|
||||
resolve_key() {
|
||||
if [[ -n "${COSIGN_KEY_FILE:-}" && -f "$COSIGN_KEY_FILE" ]]; then
|
||||
echo "$COSIGN_KEY_FILE"
|
||||
elif [[ -n "${COSIGN_PRIVATE_KEY_B64:-}" ]]; then
|
||||
local tmp_key="$OUT_DIR/.cosign.key"
|
||||
echo "$COSIGN_PRIVATE_KEY_B64" | base64 -d > "$tmp_key"
|
||||
chmod 600 "$tmp_key"
|
||||
echo "$tmp_key"
|
||||
elif [[ -f "$ROOT/tools/cosign/cosign.key" ]]; then
|
||||
echo "$ROOT/tools/cosign/cosign.key"
|
||||
elif [[ "${COSIGN_ALLOW_DEV_KEY:-0}" == "1" && -f "$ROOT/tools/cosign/cosign.dev.key" ]]; then
|
||||
echo "[info] Using development key (non-production)" >&2
|
||||
echo "$ROOT/tools/cosign/cosign.dev.key"
|
||||
else
|
||||
echo "[error] No signing key available. Set COSIGN_PRIVATE_KEY_B64 or COSIGN_ALLOW_DEV_KEY=1" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Build AOC CLI if not already built
|
||||
AOC_CLI_PROJECT="$ROOT/src/Aoc/StellaOps.Aoc.Cli/StellaOps.Aoc.Cli.csproj"
|
||||
AOC_CLI_OUT="$OUT_DIR/cli"
|
||||
|
||||
if [[ -f "$AOC_CLI_PROJECT" ]]; then
|
||||
echo "==> Building AOC CLI..."
|
||||
dotnet publish "$AOC_CLI_PROJECT" \
|
||||
-c Release \
|
||||
-o "$AOC_CLI_OUT" \
|
||||
--no-restore 2>/dev/null || echo "[info] Build skipped (may need restore)"
|
||||
else
|
||||
echo "[info] AOC CLI project not found; using placeholder"
|
||||
mkdir -p "$AOC_CLI_OUT"
|
||||
echo "AOC CLI placeholder - build from src/Aoc/StellaOps.Aoc.Cli/" > "$AOC_CLI_OUT/README.txt"
|
||||
fi
|
||||
|
||||
# Create backfill runner bundle
|
||||
echo "==> Creating backfill runner bundle..."
|
||||
RUNNER_TAR="$OUT_DIR/aoc-backfill-runner.tar.gz"
|
||||
tar -czf "$RUNNER_TAR" -C "$AOC_CLI_OUT" .
|
||||
|
||||
# Compute hash
|
||||
sha256() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
}
|
||||
RUNNER_HASH=$(sha256 "$RUNNER_TAR")
|
||||
|
||||
# Generate manifest
|
||||
echo "==> Generating manifest..."
|
||||
MANIFEST="$OUT_DIR/aoc-backfill-runner.manifest.json"
|
||||
cat > "$MANIFEST" <<EOF
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"created": "$CREATED",
|
||||
"runner": {
|
||||
"path": "aoc-backfill-runner.tar.gz",
|
||||
"sha256": "$RUNNER_HASH",
|
||||
"size": $(stat -c%s "$RUNNER_TAR" 2>/dev/null || stat -f%z "$RUNNER_TAR")
|
||||
},
|
||||
"dataset": {
|
||||
"hash": "${DATASET_HASH:-pending}",
|
||||
"status": "$( [[ -n "$DATASET_HASH" ]] && echo "available" || echo "pending-dev-rehearsal" )"
|
||||
},
|
||||
"signing": {
|
||||
"mode": "$( [[ "${COSIGN_ALLOW_DEV_KEY:-0}" == "1" ]] && echo "development" || echo "production" )"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Sign with cosign if available
|
||||
KEY_FILE=$(resolve_key) || true
|
||||
COSIGN="${COSIGN:-$ROOT/tools/cosign/cosign}"
|
||||
DSSE_OUT="$OUT_DIR/aoc-backfill-runner.dsse.json"
|
||||
|
||||
if [[ -n "${KEY_FILE:-}" ]]; then
|
||||
COSIGN_CMD="${COSIGN:-cosign}"
|
||||
if command -v cosign &>/dev/null; then
|
||||
COSIGN_CMD="cosign"
|
||||
fi
|
||||
|
||||
echo "==> Signing bundle..."
|
||||
COSIGN_PASSWORD="${COSIGN_PASSWORD:-}" "$COSIGN_CMD" sign-blob \
|
||||
--key "$KEY_FILE" \
|
||||
--bundle "$DSSE_OUT" \
|
||||
--tlog-upload=false \
|
||||
--yes \
|
||||
"$RUNNER_TAR" 2>/dev/null || echo "[info] DSSE signing skipped"
|
||||
fi
|
||||
|
||||
# Generate SBOM placeholder
|
||||
echo "==> Generating SBOM..."
|
||||
SBOM="$OUT_DIR/aoc-backfill-runner.sbom.json"
|
||||
cat > "$SBOM" <<EOF
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "aoc-backfill-runner",
|
||||
"documentNamespace": "https://stella-ops.org/sbom/aoc-backfill-runner/$CREATED",
|
||||
"creationInfo": {
|
||||
"created": "$CREATED",
|
||||
"creators": ["Tool: stellaops-aoc-packager"]
|
||||
},
|
||||
"packages": [
|
||||
{
|
||||
"name": "StellaOps.Aoc.Cli",
|
||||
"SPDXID": "SPDXRef-Package-aoc-cli",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"filesAnalyzed": false
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Generate provenance
|
||||
echo "==> Generating provenance..."
|
||||
PROVENANCE="$OUT_DIR/aoc-backfill-runner.provenance.json"
|
||||
cat > "$PROVENANCE" <<EOF
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "aoc-backfill-runner.tar.gz",
|
||||
"digest": {"sha256": "$RUNNER_HASH"}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://slsa.dev/provenance/v1",
|
||||
"predicate": {
|
||||
"buildDefinition": {
|
||||
"buildType": "https://stella-ops.org/aoc-backfill-release/v1",
|
||||
"internalParameters": {
|
||||
"created": "$CREATED",
|
||||
"datasetHash": "${DATASET_HASH:-pending}"
|
||||
}
|
||||
},
|
||||
"runDetails": {
|
||||
"builder": {"id": "https://stella-ops.org/aoc-backfill-release"}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Generate checksums
|
||||
echo "==> Generating checksums..."
|
||||
cd "$OUT_DIR"
|
||||
sha256sum aoc-backfill-runner.tar.gz aoc-backfill-runner.manifest.json aoc-backfill-runner.sbom.json > SHA256SUMS
|
||||
|
||||
# Cleanup temp key
|
||||
[[ -f "$OUT_DIR/.cosign.key" ]] && rm -f "$OUT_DIR/.cosign.key"
|
||||
|
||||
echo "==> AOC backfill packaging complete"
|
||||
echo " Runner: $RUNNER_TAR"
|
||||
echo " Manifest: $MANIFEST"
|
||||
echo " SBOM: $SBOM"
|
||||
echo " Provenance: $PROVENANCE"
|
||||
echo " Checksums: $OUT_DIR/SHA256SUMS"
|
||||
[[ -f "$DSSE_OUT" ]] && echo " DSSE: $DSSE_OUT"
|
||||
50
devops/services/aoc/supersedes-rollout.md
Normal file
50
devops/services/aoc/supersedes-rollout.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Supersedes backfill rollout plan (DEVOPS-AOC-19-101)
|
||||
|
||||
Scope: Concelier Link-Not-Merge backfill and supersedes processing once advisory_raw idempotency index is in staging.
|
||||
|
||||
## Preconditions
|
||||
- Idempotency index verified in staging (`advisory_raw` duplicate inserts rejected; log hash recorded).
|
||||
- LNM migrations 21-101/102 applied (shards, TTL, tombstones).
|
||||
- Event transport to NATS/Redis disabled during backfill to avoid noisy downstream replays.
|
||||
- Offline kit mirror includes current hashes for `advisory_raw` and backfill bundle.
|
||||
|
||||
## Rollout steps (staging → prod)
|
||||
1) **Freeze window** (announce 24h prior)
|
||||
- Pause Concelier ingest workers (`CONCELIER_INGEST_ENABLED=false`).
|
||||
- Stop outbox publisher or point to blackhole NATS subject.
|
||||
2) **Dry-run (staging)**
|
||||
- Run backfill job with `--dry-run` to emit counts only.
|
||||
- Verify: new supersedes records count == expected; no write errors; idempotency violations = 0.
|
||||
- Capture logs + SHA256 of generated report.
|
||||
3) **Prod execution**
|
||||
- Run backfill job with `--batch-size=500` and `--stop-on-error`.
|
||||
- Monitor: insert rate, error rate, Mongo oplog lag; target <5% CPU on primary.
|
||||
4) **Validation**
|
||||
- Run consistency check:
|
||||
- `advisory_observations` count stable (no drop).
|
||||
- Supersedes edges present for all prior conflicts.
|
||||
- Idempotency index hit rate <0.1%.
|
||||
- Run API spot check: `/advisories/summary` returns supersedes metadata; `advisory.linkset.updated` events absent during freeze.
|
||||
5) **Unfreeze**
|
||||
- Re-enable ingest + outbox publisher.
|
||||
- Trigger single `advisory.observation.updated@1` replay to confirm event path is healthy.
|
||||
|
||||
## Rollback
|
||||
- If errors >0 or idempotency violations observed:
|
||||
- Stop job, keep ingest paused.
|
||||
- Run rollback script `ops/devops/scripts/rollback-lnm-backfill.js` to remove supersedes/tombstones inserted in current window.
|
||||
- Restore Mongo from last checkpointed snapshot if rollback script fails.
|
||||
|
||||
## Evidence to capture
|
||||
- Job command + arguments.
|
||||
- SHA256 of backfill bundle and report.
|
||||
- Idempotency violation count.
|
||||
- Post-run consistency report (JSON) stored under `ops/devops/artifacts/aoc-supersedes/<timestamp>/`.
|
||||
|
||||
## Monitoring/Alerts
|
||||
- Add temporary Grafana panel for idempotency violations and Mongo ops/sec during job.
|
||||
- Alert if job runtime exceeds 2h or if oplog lag > 60s.
|
||||
|
||||
## Owners
|
||||
- Run: DevOps Guild
|
||||
- Approvals: Concelier Storage Guild + Platform Security
|
||||
20
devops/services/authority/AGENTS.md
Normal file
20
devops/services/authority/AGENTS.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Authority DevOps Crew
|
||||
|
||||
## Mission
|
||||
Operate and harden the StellaOps Authority platform in production and air-gapped environments: container images, deployment assets, observability defaults, backup/restore, and runtime key management.
|
||||
|
||||
## Focus Areas
|
||||
- **Build & Packaging** – Dockerfiles, OCI bundles, offline artefact refresh.
|
||||
- **Deployment Tooling** – Compose/Kubernetes manifests, secrets bootstrap, upgrade paths.
|
||||
- **Observability** – Logging defaults, metrics/trace exporters, dashboards, alert policies.
|
||||
- **Continuity & Security** – Backup/restore guides, key rotation playbooks, revocation propagation.
|
||||
|
||||
## Working Agreements
|
||||
- Track work directly in the relevant `docs/implplan/SPRINT_*.md` rows (TODO → DOING → DONE/BLOCKED); keep entries dated.
|
||||
- Validate container changes with the CI pipeline (`ops/authority` GitHub workflow) before marking DONE.
|
||||
- Update operator documentation in `docs/` together with any behavioural change.
|
||||
- Coordinate with Authority Core and Security Guild before altering sensitive defaults (rate limits, crypto providers, revocation jobs).
|
||||
|
||||
## Required Reading
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/modules/airgap/airgap-mode.md`
|
||||
38
devops/services/authority/Dockerfile
Normal file
38
devops/services/authority/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
|
||||
#
|
||||
# StellaOps Authority – distroless container build
|
||||
# Produces a minimal image containing the Authority host and its plugins.
|
||||
#
|
||||
|
||||
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/nightly/sdk:10.0
|
||||
ARG RUNTIME_IMAGE=gcr.io/distroless/dotnet/aspnet:latest
|
||||
|
||||
FROM ${SDK_IMAGE} AS build
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Restore & publish
|
||||
COPY . .
|
||||
RUN dotnet restore src/StellaOps.sln
|
||||
RUN dotnet publish src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM ${RUNTIME_IMAGE} AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
ENV STELLAOPS_AUTHORITY__PLUGINDIRECTORIES__0=/app/plugins
|
||||
ENV STELLAOPS_AUTHORITY__PLUGINS__CONFIGURATIONDIRECTORY=/app/etc/authority.plugins
|
||||
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Provide writable mount points for configs/keys/plugins
|
||||
VOLUME ["/app/etc", "/app/plugins", "/app/keys"]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["dotnet", "StellaOps.Authority.dll"]
|
||||
62
devops/services/authority/README.md
Normal file
62
devops/services/authority/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# StellaOps Authority Container Scaffold
|
||||
|
||||
This directory provides a distroless Dockerfile and `docker-compose` sample for bootstrapping the Authority service alongside MongoDB (required) and Redis (optional).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine 25+ and Compose V2
|
||||
- .NET 10 preview SDK (only required when building locally outside of Compose)
|
||||
- Populated Authority configuration at `etc/authority.yaml` and plugin manifests under `etc/authority.plugins/`
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# 1. Ensure configuration files exist (copied from etc/authority.yaml.sample, etc/authority.plugins/*.yaml)
|
||||
# 2. Build and start the stack
|
||||
docker compose -f ops/authority/docker-compose.authority.yaml up --build
|
||||
```
|
||||
|
||||
`authority.yaml` is mounted read-only at `/etc/authority.yaml` inside the container. Plugin manifests are mounted to `/app/etc/authority.plugins`. Update the issuer URL plus any Mongo credentials in the compose file or via an `.env`.
|
||||
|
||||
To run with pre-built images, replace the `build:` block in the compose file with an `image:` reference.
|
||||
|
||||
## Volumes
|
||||
|
||||
- `mongo-data` – persists MongoDB state.
|
||||
- `redis-data` – optional Redis persistence (enable the service before use).
|
||||
- `authority-keys` – writable volume for Authority signing keys.
|
||||
|
||||
## Environment overrides
|
||||
|
||||
Key environment variables (mirroring `StellaOpsAuthorityOptions`):
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `STELLAOPS_AUTHORITY__ISSUER` | Public issuer URL advertised by Authority |
|
||||
| `STELLAOPS_AUTHORITY__PLUGINDIRECTORIES__0` | Primary plugin binaries directory inside the container |
|
||||
| `STELLAOPS_AUTHORITY__PLUGINS__CONFIGURATIONDIRECTORY` | Path to plugin manifest directory |
|
||||
|
||||
For additional options, see `etc/authority.yaml.sample`.
|
||||
|
||||
> **Graph Explorer reminder:** When enabling Cartographer or Graph API components, update `etc/authority.yaml` so the `cartographer-service` client includes `properties.serviceIdentity: "cartographer"` and a tenant hint. Authority now rejects `graph:write` tokens that lack this marker, so existing deployments must apply the update before rolling out the new build.
|
||||
|
||||
> **Console endpoint reminder:** The Console UI now calls `/console/tenants`, `/console/profile`, and `/console/token/introspect`. Reverse proxies must forward the `X-Stella-Tenant` header (derived from the access token) so Authority can enforce tenancy; audit events are logged under `authority.console.*`. Admin actions obey a five-minute fresh-auth window reported by `/console/profile`, so keep session timeout prompts aligned with that value.
|
||||
|
||||
## Key rotation automation (OPS3)
|
||||
|
||||
The `key-rotation.sh` helper wraps the `/internal/signing/rotate` endpoint delivered with CORE10. It can run in CI/CD once the new PEM key is staged on the Authority host volume.
|
||||
|
||||
```bash
|
||||
AUTHORITY_BOOTSTRAP_KEY=$(cat ~/.secrets/authority-bootstrap.key) \
|
||||
./key-rotation.sh \
|
||||
--authority-url https://authority.stella-ops.local \
|
||||
--key-id authority-signing-2025 \
|
||||
--key-path ../certificates/authority-signing-2025.pem \
|
||||
--meta rotatedBy=pipeline --meta changeTicket=OPS-1234
|
||||
```
|
||||
|
||||
- `--key-path` should resolve from the Authority content root (same as `docs/11_AUTHORITY.md` SOP).
|
||||
- Provide `--source`/`--provider` if the key loader differs from the default file-based provider.
|
||||
- Pass `--dry-run` during rehearsals to inspect the JSON payload without invoking the API.
|
||||
|
||||
After rotation, export a fresh revocation bundle (`stellaops-cli auth revoke export`) so downstream mirrors consume signatures from the new `kid`. The canonical operational steps live in `docs/11_AUTHORITY.md` – make sure any local automation keeps that guide as source of truth.
|
||||
5
devops/services/authority/TASKS.completed.md
Normal file
5
devops/services/authority/TASKS.completed.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Completed Tasks
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
| OPS3.KEY-ROTATION | DONE (2025-10-12) | DevOps Crew, Authority Core | CORE10.JWKS | Implement key rotation tooling + pipeline hook once rotating JWKS lands. Document SOP and secret handling. | ✅ CLI/script rotates keys + updates JWKS; ✅ Pipeline job documented; ✅ docs/ops runbook updated. |
|
||||
58
devops/services/authority/docker-compose.authority.yaml
Normal file
58
devops/services/authority/docker-compose.authority.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
authority:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: ops/authority/Dockerfile
|
||||
image: stellaops-authority:dev
|
||||
container_name: stellaops-authority
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_started
|
||||
environment:
|
||||
# Override issuer to match your deployment URL.
|
||||
STELLAOPS_AUTHORITY__ISSUER: "https://authority.localtest.me"
|
||||
# Point the Authority host at the Mongo instance defined below.
|
||||
STELLAOPS_AUTHORITY__PLUGINDIRECTORIES__0: "/app/plugins"
|
||||
STELLAOPS_AUTHORITY__PLUGINS__CONFIGURATIONDIRECTORY: "/app/etc/authority.plugins"
|
||||
volumes:
|
||||
# Mount Authority configuration + plugins (edit etc/authority.yaml before running).
|
||||
- ../../etc/authority.yaml:/etc/authority.yaml:ro
|
||||
- ../../etc/authority.plugins:/app/etc/authority.plugins:ro
|
||||
# Optional: persist plugin binaries or key material outside the container.
|
||||
- authority-keys:/app/keys
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
mongo:
|
||||
image: mongo:7
|
||||
container_name: stellaops-authority-mongo
|
||||
command: ["mongod", "--bind_ip_all"]
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: stellaops
|
||||
MONGO_INITDB_ROOT_PASSWORD: stellaops
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: unless-stopped
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8-alpine
|
||||
container_name: stellaops-authority-valkey
|
||||
command: ["valkey-server", "--save", "60", "1"]
|
||||
volumes:
|
||||
- valkey-data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
# Uncomment to enable if/when Authority consumes Valkey.
|
||||
# deploy:
|
||||
# replicas: 0
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
valkey-data:
|
||||
authority-keys:
|
||||
189
devops/services/authority/key-rotation.sh
Normal file
189
devops/services/authority/key-rotation.sh
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: key-rotation.sh --authority-url URL --api-key TOKEN --key-id ID --key-path PATH [options]
|
||||
|
||||
Required flags:
|
||||
-u, --authority-url Base Authority URL (e.g. https://authority.example.com)
|
||||
-k, --api-key Bootstrap API key (x-stellaops-bootstrap-key header)
|
||||
-i, --key-id Identifier (kid) for the new signing key
|
||||
-p, --key-path Path (relative to Authority content root or absolute) where the PEM key lives
|
||||
|
||||
Optional flags:
|
||||
-s, --source Key source loader identifier (default: file)
|
||||
-a, --algorithm Signing algorithm (default: ES256)
|
||||
--provider Preferred crypto provider name
|
||||
-m, --meta key=value Additional metadata entries for the rotation record (repeatable)
|
||||
--dry-run Print the JSON payload instead of invoking the API
|
||||
-h, --help Show this help
|
||||
|
||||
Environment fallbacks:
|
||||
AUTHORITY_URL, AUTHORITY_BOOTSTRAP_KEY, AUTHORITY_KEY_SOURCE, AUTHORITY_KEY_PROVIDER
|
||||
|
||||
Example:
|
||||
AUTHORITY_BOOTSTRAP_KEY=$(cat key.txt) \\
|
||||
./key-rotation.sh -u https://authority.local \\
|
||||
-i authority-signing-2025 \\
|
||||
-p ../certificates/authority-signing-2025.pem \\
|
||||
-m rotatedBy=pipeline -m ticket=OPS-1234
|
||||
USAGE
|
||||
}
|
||||
|
||||
require_python() {
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON_BIN=python3
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PYTHON_BIN=python
|
||||
else
|
||||
echo "error: python3 (or python) is required for JSON encoding" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
json_quote() {
|
||||
"$PYTHON_BIN" - "$1" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps(sys.argv[1]))
|
||||
PY
|
||||
}
|
||||
|
||||
AUTHORITY_URL="${AUTHORITY_URL:-}"
|
||||
API_KEY="${AUTHORITY_BOOTSTRAP_KEY:-}"
|
||||
KEY_ID=""
|
||||
KEY_PATH=""
|
||||
SOURCE="${AUTHORITY_KEY_SOURCE:-file}"
|
||||
ALGORITHM="ES256"
|
||||
PROVIDER="${AUTHORITY_KEY_PROVIDER:-}"
|
||||
DRY_RUN=false
|
||||
declare -a METADATA=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-u|--authority-url)
|
||||
AUTHORITY_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-k|--api-key)
|
||||
API_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--key-id)
|
||||
KEY_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
-p|--key-path)
|
||||
KEY_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-s|--source)
|
||||
SOURCE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--algorithm)
|
||||
ALGORITHM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--provider)
|
||||
PROVIDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--meta)
|
||||
METADATA+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$AUTHORITY_URL" || -z "$API_KEY" || -z "$KEY_ID" || -z "$KEY_PATH" ]]; then
|
||||
echo "error: missing required arguments" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$AUTHORITY_URL" in
|
||||
http://*|https://*) ;;
|
||||
*)
|
||||
echo "error: --authority-url must include scheme (http/https)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
require_python
|
||||
|
||||
payload="{"
|
||||
payload+="\"keyId\":$(json_quote "$KEY_ID"),"
|
||||
payload+="\"location\":$(json_quote "$KEY_PATH"),"
|
||||
payload+="\"source\":$(json_quote "$SOURCE"),"
|
||||
payload+="\"algorithm\":$(json_quote "$ALGORITHM"),"
|
||||
if [[ -n "$PROVIDER" ]]; then
|
||||
payload+="\"provider\":$(json_quote "$PROVIDER"),"
|
||||
fi
|
||||
|
||||
if [[ ${#METADATA[@]} -gt 0 ]]; then
|
||||
payload+="\"metadata\":{"
|
||||
for entry in "${METADATA[@]}"; do
|
||||
if [[ "$entry" != *=* ]]; then
|
||||
echo "warning: ignoring metadata entry '$entry' (expected key=value)" >&2
|
||||
continue
|
||||
fi
|
||||
key="${entry%%=*}"
|
||||
value="${entry#*=}"
|
||||
payload+="$(json_quote "$key"):$(json_quote "$value"),"
|
||||
done
|
||||
if [[ "${payload: -1}" == "," ]]; then
|
||||
payload="${payload::-1}"
|
||||
fi
|
||||
payload+="},"
|
||||
fi
|
||||
|
||||
if [[ "${payload: -1}" == "," ]]; then
|
||||
payload="${payload::-1}"
|
||||
fi
|
||||
payload+="}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "# Dry run payload:"
|
||||
echo "$payload"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp_response="$(mktemp)"
|
||||
cleanup() { rm -f "$tmp_response"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
http_code=$(curl -sS -o "$tmp_response" -w "%{http_code}" \
|
||||
-X POST "${AUTHORITY_URL%/}/internal/signing/rotate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-stellaops-bootstrap-key: $API_KEY" \
|
||||
--data "$payload")
|
||||
|
||||
if [[ "$http_code" != "200" && "$http_code" != "201" ]]; then
|
||||
echo "error: rotation API returned HTTP $http_code" >&2
|
||||
cat "$tmp_response" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Rotation request accepted (HTTP $http_code). Response:"
|
||||
cat "$tmp_response"
|
||||
|
||||
echo
|
||||
echo "Fetching JWKS to confirm active key..."
|
||||
curl -sS "${AUTHORITY_URL%/}/jwks" || true
|
||||
echo
|
||||
echo "Done. Remember to update authority.yaml with the new key metadata to keep restarts consistent."
|
||||
32
devops/services/ci-110-runner/README.md
Normal file
32
devops/services/ci-110-runner/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
CI runner for **DEVOPS-CI-110-001** (Concelier + Excititor smoke)
|
||||
==================================================================
|
||||
|
||||
Scope
|
||||
-----
|
||||
- Warm NuGet cache from `local-nugets`, `.nuget/packages`, and (optionally) NuGet.org.
|
||||
- Ensure OpenSSL 1.1 is present (installs `libssl1.1` when available via `apt-get`).
|
||||
- Run lightweight slices:
|
||||
- Concelier WebService: `HealthAndReadyEndpointsRespond`
|
||||
- Excititor WebService: `AirgapImportEndpointTests*`
|
||||
- Emit TRX + logs to `ops/devops/artifacts/ci-110/<timestamp>/`.
|
||||
|
||||
Usage
|
||||
-----
|
||||
```bash
|
||||
export NUGET_SOURCES="/mnt/e/dev/git.stella-ops.org/local-nugets;/mnt/e/dev/git.stella-ops.org/.nuget/packages;https://api.nuget.org/v3/index.json"
|
||||
export TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ) # optional, for reproducible paths
|
||||
bash ops/devops/ci-110-runner/run-ci-110.sh
|
||||
```
|
||||
|
||||
Artifacts
|
||||
---------
|
||||
- TRX: `ops/devops/artifacts/ci-110/<timestamp>/trx/`
|
||||
- `concelier-health.trx` (1 test)
|
||||
- `excititor-airgapimport.fqn.trx` (2 tests)
|
||||
- Logs + restores under `ops/devops/artifacts/ci-110/<timestamp>/logs/`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
- The runner uses `--no-build` on test slices; prior restores are included in the script.
|
||||
- If OpenSSL 1.1 is not present and `apt-get` cannot install `libssl1.1`, set `LD_LIBRARY_PATH` to a pre-installed OpenSSL 1.1 location before running.
|
||||
- Extend the runner by adding more `run_test_slice` calls for additional suites; keep filters tight to avoid long hangs on constrained CI.
|
||||
92
devops/services/ci-110-runner/run-ci-110.sh
Normal file
92
devops/services/ci-110-runner/run-ci-110.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# CI helper for DEVOPS-CI-110-001
|
||||
# - Warms NuGet cache from local sources
|
||||
# - Ensures OpenSSL 1.1 compatibility if available
|
||||
# - Runs targeted Concelier and Excititor test slices with TRX output
|
||||
# - Writes artefacts under ops/devops/artifacts/ci-110/<timestamp>/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${ROOT:-$(git rev-parse --show-toplevel)}"
|
||||
TIMESTAMP="${TIMESTAMP:-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||
ARTIFACT_ROOT="${ARTIFACT_ROOT:-"$ROOT/ops/devops/artifacts/ci-110/$TIMESTAMP"}"
|
||||
LOG_DIR="$ARTIFACT_ROOT/logs"
|
||||
TRX_DIR="$ARTIFACT_ROOT/trx"
|
||||
|
||||
NUGET_SOURCES_DEFAULT="$ROOT/local-nugets;$ROOT/.nuget/packages;https://api.nuget.org/v3/index.json"
|
||||
NUGET_SOURCES="${NUGET_SOURCES:-$NUGET_SOURCES_DEFAULT}"
|
||||
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
export DOTNET_RESTORE_DISABLE_PARALLEL="${DOTNET_RESTORE_DISABLE_PARALLEL:-1}"
|
||||
|
||||
mkdir -p "$LOG_DIR" "$TRX_DIR"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"
|
||||
}
|
||||
|
||||
ensure_openssl11() {
|
||||
if openssl version 2>/dev/null | grep -q "1\\.1."; then
|
||||
log "OpenSSL 1.1 detected: $(openssl version)"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
log "OpenSSL 1.1 not found; attempting install via apt-get (libssl1.1)"
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null || true
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libssl1.1 || true
|
||||
if openssl version 2>/dev/null | grep -q "1\\.1."; then
|
||||
log "OpenSSL 1.1 available after install: $(openssl version)"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
log "OpenSSL 1.1 still unavailable. Provide it via LD_LIBRARY_PATH if required."
|
||||
}
|
||||
|
||||
restore_solution() {
|
||||
local sln="$1"
|
||||
log "Restore $sln"
|
||||
dotnet restore "$sln" --source "$NUGET_SOURCES" --verbosity minimal | tee "$LOG_DIR/restore-$(basename "$sln").log"
|
||||
}
|
||||
|
||||
run_test_slice() {
|
||||
local proj="$1"
|
||||
local filter="$2"
|
||||
local name="$3"
|
||||
log "Test $name ($proj, filter='$filter')"
|
||||
dotnet test "$proj" \
|
||||
-c Debug \
|
||||
--no-build \
|
||||
${filter:+--filter "$filter"} \
|
||||
--logger "trx;LogFileName=${name}.trx" \
|
||||
--results-directory "$TRX_DIR" \
|
||||
--blame-hang \
|
||||
--blame-hang-timeout 8m \
|
||||
--blame-hang-dump-type none \
|
||||
| tee "$LOG_DIR/test-${name}.log"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "Starting CI-110 runner; artefacts -> $ARTIFACT_ROOT"
|
||||
ensure_openssl11
|
||||
|
||||
restore_solution "$ROOT/concelier-webservice.slnf"
|
||||
restore_solution "$ROOT/src/Excititor/StellaOps.Excititor.sln"
|
||||
|
||||
# Concelier: lightweight health slice to validate runner + Mongo wiring
|
||||
run_test_slice "$ROOT/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj" \
|
||||
"HealthAndReadyEndpointsRespond" \
|
||||
"concelier-health"
|
||||
|
||||
# Excititor: airgap import surface (chunk-path) smoke
|
||||
run_test_slice "$ROOT/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj" \
|
||||
"FullyQualifiedName~AirgapImportEndpointTests" \
|
||||
"excititor-airgapimport"
|
||||
|
||||
log "Done. TRX files in $TRX_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
11
devops/services/ci-110-runner/test-filters.md
Normal file
11
devops/services/ci-110-runner/test-filters.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# CI-110 runner filters (Concelier/Excititor smoke)
|
||||
|
||||
## Concelier
|
||||
- WebService health: `HealthAndReadyEndpointsRespond`
|
||||
- Storage.Mongo job store: `FullyQualifiedName~MongoJobStore`
|
||||
- WebService orchestrator endpoints: TODO (tests not yet present; add to WebService.Tests then filter with `FullyQualifiedName~Orchestrator`)
|
||||
|
||||
## Excititor
|
||||
- WebService airgap import: `FullyQualifiedName~AirgapImportEndpointTests`
|
||||
|
||||
Artifacts are written under `ops/devops/artifacts/ci-110/<timestamp>/` by `run-ci-110.sh`.
|
||||
26
devops/services/concelier-ci-runner/README.md
Normal file
26
devops/services/concelier-ci-runner/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Concelier CI Runner Harness (DEVOPS-CONCELIER-CI-24-101)
|
||||
|
||||
Purpose: provide a deterministic, offline-friendly harness that restores, builds, and runs Concelier WebService + Storage Mongo tests with warmed NuGet cache and TRX/binlog artefacts for downstream sprints (Concelier II/III).
|
||||
|
||||
Usage
|
||||
- From repo root run: `ops/devops/concelier-ci-runner/run-concelier-ci.sh`
|
||||
- Outputs land in `ops/devops/artifacts/concelier-ci/<UTC timestamp>/`:
|
||||
- `build.binlog` (solution build)
|
||||
- `tests/webservice.trx`, `tests/storage.trx` (VSTest results)
|
||||
- per-project `.dmp`/logs if failures occur
|
||||
- `summary.json` (paths + hashes)
|
||||
|
||||
Environment
|
||||
- Defaults: `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1`, `NUGET_PACKAGES=$REPO/.nuget/packages`.
|
||||
- Uses local feed `local-nugets/` first, then NuGet.org (can be overridden via `NUGET_SOURCES`).
|
||||
- No external services required; Mongo2Go provides ephemeral Mongo for tests.
|
||||
|
||||
What it does
|
||||
1) Warm NuGet cache from `local-nugets/` into `$NUGET_PACKAGES` for offline/air-gap parity.
|
||||
2) `dotnet restore` + `dotnet build` on `concelier-webservice.slnf` with `/bl`.
|
||||
3) Run WebService and Storage.Mongo test projects with TRX output and without rebuild (`--no-build`).
|
||||
4) Emit a concise `summary.json` listing artefacts and SHA256s for reproducibility.
|
||||
|
||||
Notes
|
||||
- Keep test filters narrow if you need faster runs; edit `TEST_FILTER` env var (default empty = run all tests).
|
||||
- Artefacts are timestamped UTC to keep ordering deterministic in pipelines; consumers should sort by path.
|
||||
75
devops/services/concelier-ci-runner/run-concelier-ci.sh
Normal file
75
devops/services/concelier-ci-runner/run-concelier-ci.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Concelier CI runner harness (DEVOPS-CONCELIER-CI-24-101)
|
||||
# Produces warmed-cache restore, build binlog, and TRX outputs for WebService + Storage Mongo tests.
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
out_dir="$repo_root/ops/devops/artifacts/concelier-ci/$ts"
|
||||
logs_dir="$out_dir/tests"
|
||||
mkdir -p "$logs_dir"
|
||||
|
||||
# Deterministic env
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT:-1}
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=${DOTNET_SKIP_FIRST_TIME_EXPERIENCE:-1}
|
||||
export NUGET_PACKAGES=${NUGET_PACKAGES:-$repo_root/.nuget/packages}
|
||||
export NUGET_SOURCES=${NUGET_SOURCES:-"$repo_root/local-nugets;$repo_root/.nuget/packages"}
|
||||
export TEST_FILTER=${TEST_FILTER:-""}
|
||||
export DOTNET_RESTORE_DISABLE_PARALLEL=${DOTNET_RESTORE_DISABLE_PARALLEL:-1}
|
||||
|
||||
# Warm NuGet cache from local feed for offline/airgap parity
|
||||
mkdir -p "$NUGET_PACKAGES"
|
||||
rsync -a "$repo_root/local-nugets/" "$NUGET_PACKAGES/" >/dev/null 2>&1 || true
|
||||
|
||||
# Restore with deterministic sources
|
||||
restore_sources=()
|
||||
IFS=';' read -ra SRC_ARR <<< "$NUGET_SOURCES"
|
||||
for s in "${SRC_ARR[@]}"; do
|
||||
[[ -n "$s" ]] && restore_sources+=(--source "$s")
|
||||
done
|
||||
|
||||
dotnet restore "$repo_root/concelier-webservice.slnf" --ignore-failed-sources "${restore_sources[@]}"
|
||||
|
||||
# Build with binlog
|
||||
build_binlog="$out_dir/build.binlog"
|
||||
dotnet build "$repo_root/concelier-webservice.slnf" -c Debug /p:ContinuousIntegrationBuild=true /bl:"$build_binlog"
|
||||
|
||||
common_test_args=( -c Debug --no-build --results-directory "$logs_dir" )
|
||||
if [[ -n "$TEST_FILTER" ]]; then
|
||||
common_test_args+=( --filter "$TEST_FILTER" )
|
||||
fi
|
||||
|
||||
# WebService tests
|
||||
web_trx="webservice.trx"
|
||||
dotnet test "$repo_root/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj" \
|
||||
"${common_test_args[@]}" \
|
||||
--logger "trx;LogFileName=$web_trx"
|
||||
|
||||
# Storage Mongo tests
|
||||
storage_trx="storage.trx"
|
||||
dotnet test "$repo_root/src/Concelier/__Tests/StellaOps.Concelier.Storage.Mongo.Tests/StellaOps.Concelier.Storage.Mongo.Tests.csproj" \
|
||||
"${common_test_args[@]}" \
|
||||
--logger "trx;LogFileName=$storage_trx"
|
||||
|
||||
# Summarize artefacts (relative paths to repo root)
|
||||
summary="$out_dir/summary.json"
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "timestamp_utc": "%s",\n' "$ts"
|
||||
printf ' "build_binlog": "%s",\n' "${build_binlog#${repo_root}/}"
|
||||
printf ' "tests": [\n'
|
||||
printf ' {"project": "WebService", "trx": "%s"},\n' "${logs_dir#${repo_root}/}/$web_trx"
|
||||
printf ' {"project": "Storage.Mongo", "trx": "%s"}\n' "${logs_dir#${repo_root}/}/$storage_trx"
|
||||
printf ' ],\n'
|
||||
printf ' "nuget_packages": "%s",\n' "${NUGET_PACKAGES#${repo_root}/}"
|
||||
printf ' "sources": [\n'
|
||||
for i in "${!SRC_ARR[@]}"; do
|
||||
sep=","; [[ $i -eq $((${#SRC_ARR[@]}-1)) ]] && sep=""
|
||||
printf ' "%s"%s\n' "${SRC_ARR[$i]}" "$sep"
|
||||
done
|
||||
printf ' ]\n'
|
||||
printf '}\n'
|
||||
} > "$summary"
|
||||
|
||||
echo "Artifacts written to ${out_dir#${repo_root}/}"
|
||||
53
devops/services/concelier-config/lnm-release-plan.md
Normal file
53
devops/services/concelier-config/lnm-release-plan.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Concelier LNM Release Plan (DEVOPS-LNM-21-101-REL / 102-REL / 103-REL)
|
||||
|
||||
Scope: package and publish Link-Not-Merge migrations/backfill/object-store seeds for release and offline kits.
|
||||
|
||||
## Artefacts
|
||||
- Migration bundles:
|
||||
- 21-101 shard/index migrations (`EnsureLinkNotMergeShardingAndTtlMigration`)
|
||||
- 21-102 backfill/tombstone/rollback scripts
|
||||
- 21-103 object-store seed bundle (once contract final)
|
||||
- Checksums (`SHA256SUMS`, signed)
|
||||
- SBOMs (spdx.json) for migration runner image/tooling
|
||||
- Cosign attestations for images/bundles
|
||||
- Offline kit slice tarball with all above + DSSE manifest
|
||||
|
||||
## Pipeline outline
|
||||
1) Build migration runner image (dotnet) with migrations baked; generate SBOM; pin digest.
|
||||
2) Export migration scripts/bundles to `artifacts/lnm/`.
|
||||
3) Create offline bundle:
|
||||
- `migrations/21-101/` (DLLs, scripts, README)
|
||||
- `migrations/21-102/` (backfill, rollback, README)
|
||||
- `seeds/object-store/` (placeholder until 21-103 dev output)
|
||||
- `SHA256SUMS` + `.sig`
|
||||
- SBOMs + cosign attestations
|
||||
4) Verification stage:
|
||||
- `dotnet test` on migration runner
|
||||
- `cosign verify-blob` for bundles
|
||||
- `sha256sum --check`
|
||||
5) Publish:
|
||||
- Upload to release bucket + offline kit
|
||||
- Record manifest (hashes, versions, digests)
|
||||
|
||||
## Runbook (apply in staging → prod)
|
||||
1) Take Mongo backup; freeze Concelier ingest.
|
||||
2) Apply 21-101 migrations (shards/TTL) — idempotent; record duration.
|
||||
3) Run 21-102 backfill with `--batch-size=500 --stop-on-error`; capture report hash.
|
||||
4) Validate counts (observations/linksets/events) and shard balance.
|
||||
5) Enable outbox publishers; monitor lag and errors.
|
||||
6) (When ready) apply 21-103 object-store migration: move raw payloads to object store; verify CAS URIs; keep GridFS read-only during move.
|
||||
|
||||
## Rollback
|
||||
- 21-101: restore from backup if shard layout breaks; migrations are idempotent.
|
||||
- 21-102: run rollback script (`ops/devops/scripts/rollback-lnm-backfill.js`); if inconsistent, restore backup.
|
||||
- 21-103: switch back to GridFS URI map; restore seeds.
|
||||
|
||||
## Monitoring/alerts
|
||||
- Migration error count > 0
|
||||
- Mongo oplog lag > 60s during backfill
|
||||
- Outbox backlog growth post-unfreeze
|
||||
|
||||
## Owners
|
||||
- DevOps Guild (pipeline + rollout)
|
||||
- Concelier Storage Guild (migration content)
|
||||
- Platform Security (signing policy)
|
||||
38
devops/services/console/Dockerfile.runner
Normal file
38
devops/services/console/Dockerfile.runner
Normal file
@@ -0,0 +1,38 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# Offline-friendly console CI runner image with pre-baked npm and Playwright caches (DEVOPS-CONSOLE-23-001)
|
||||
ARG BASE_IMAGE=node:20-bookworm-slim
|
||||
ARG APP_DIR=src/Web/StellaOps.Web
|
||||
ARG SOURCE_DATE_EPOCH=1704067200
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
NPM_CONFIG_FUND=false \
|
||||
NPM_CONFIG_AUDIT=false \
|
||||
NPM_CONFIG_PROGRESS=false \
|
||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
|
||||
NPM_CONFIG_CACHE=/home/node/.npm \
|
||||
CI=true
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends git ca-certificates dumb-init wget curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /tmp/console-seed
|
||||
COPY ${APP_DIR}/package.json ${APP_DIR}/package-lock.json ./
|
||||
|
||||
ENV npm_config_cache=/tmp/npm-cache
|
||||
RUN npm ci --cache ${npm_config_cache} --prefer-offline --no-audit --progress=false --ignore-scripts && \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright npx playwright install chromium --with-deps && \
|
||||
rm -rf node_modules
|
||||
|
||||
RUN install -d -o node -g node /home/node/.npm /home/node/.cache && \
|
||||
mv /tmp/npm-cache /home/node/.npm && \
|
||||
mv /tmp/ms-playwright /home/node/.cache/ms-playwright && \
|
||||
chown -R node:node /home/node/.npm /home/node/.cache
|
||||
|
||||
WORKDIR /workspace
|
||||
USER node
|
||||
ENTRYPOINT ["/usr/bin/dumb-init","--"]
|
||||
CMD ["/bin/bash"]
|
||||
44
devops/services/console/README.md
Normal file
44
devops/services/console/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Console CI runner (offline-friendly)
|
||||
|
||||
Status: runner spec + CI now wired to PRs; runner image scaffold + CI build workflow now available with baked npm + Playwright cache.
|
||||
|
||||
## Runner profile
|
||||
- OS: Ubuntu 22.04 LTS (x86_64) with Docker available for Playwright deps if needed.
|
||||
- Node: 20.x (LTS). Enable corepack; prefer npm (default) to avoid extra downloads.
|
||||
- Caches:
|
||||
- npm: `~/.npm` keyed by `src/Web/package-lock.json` hash.
|
||||
- Playwright: `~/.cache/ms-playwright` pre-seeded with Chromium so `npm test -- --browsers=ChromeHeadless` can run offline. Seed once using `npx playwright install chromium` on a connected runner, then snapshot the directory into the runner image.
|
||||
- Angular build cache: optional `~/.cache/angular` if using angular.json cache; safe to keep.
|
||||
- Artifacts retention: keep lint/test/build outputs 14 days; limit to 500 MB per run (coverage + dist + test reports). Artifacts path: `artifacts/` (dist, coverage, junit/trx if produced).
|
||||
|
||||
## Pipeline steps (expected)
|
||||
1) Checkout
|
||||
2) Node 20 setup with npm cache restore (package-lock at `src/Web/package-lock.json`).
|
||||
3) Install: `npm ci --prefer-offline --no-audit --progress=false` in `src/Web`.
|
||||
4) Lint: `npm run lint -- --no-progress`.
|
||||
5) Unit: `npm test -- --watch=false --browsers=ChromeHeadless --no-progress` (headless Chromium from pre-seeded cache).
|
||||
6) Build: `npm run build -- --configuration=production --progress=false`.
|
||||
7) Artifact collect: `dist/`, `coverage/`, any `test-results/**`.
|
||||
|
||||
## Offline/airgap notes
|
||||
- Do not hit external registries during CI; rely on pre-seeded npm mirror or cached tarballs. Runner image should contain npm cache prime. If mirror is used, set `NPM_CONFIG_REGISTRY=https://registry.npmjs.org` equivalent mirror URL inside the runner; default pipeline does not hard-code it.
|
||||
- Playwright browsers must be pre-baked; the workflow will not download them.
|
||||
|
||||
### Runner image (with baked caches)
|
||||
- Dockerfile: `ops/devops/console/Dockerfile.runner` (Node 20, npm cache, Playwright Chromium cache). Builds with `npm ci` + `playwright install chromium --with-deps` during the image build.
|
||||
- Build locally: `IMAGE_TAG=stellaops/console-runner:offline OUTPUT_TAR=ops/devops/artifacts/console-runner/console-runner.tar ops/devops/console/build-runner-image.sh`
|
||||
- `OUTPUT_TAR` optional; when set, the script saves the image for airgap transport.
|
||||
- Runner expectations: `NPM_CONFIG_CACHE=~/.npm`, `PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright` (paths already baked). Register the runner with a label (e.g., `console-ci`) and point `.gitea/workflows/console-ci.yml` at that runner pool.
|
||||
- CI build helper: `ops/devops/console/build-runner-image-ci.sh` wraps the build, sets a run-scoped tag, emits metadata JSON, and saves a tarball under `ops/devops/artifacts/console-runner/`.
|
||||
- CI workflow: `.gitea/workflows/console-runner-image.yml` (manual + path-trigger) builds the runner image and uploads the tarball + metadata as an artifact named `console-runner-image-<run_id>`.
|
||||
|
||||
### Seeding Playwright cache (one-time per runner image, host-based option)
|
||||
```bash
|
||||
ops/devops/console/seed_playwright.sh
|
||||
# then bake ~/.cache/ms-playwright into the runner image or mount it on the agent
|
||||
```
|
||||
|
||||
## How to run
|
||||
- PR-triggered via `.gitea/workflows/console-ci.yml`; restrict runners to images with baked Playwright cache.
|
||||
- Manual `workflow_dispatch` remains available for dry runs or cache updates.
|
||||
- To refresh the runner image, run the `console-runner-image` workflow or execute `ops/devops/console/build-runner-image-ci.sh` locally to generate a tarball and metadata for distribution.
|
||||
86
devops/services/console/build-console-image.sh
Normal file
86
devops/services/console/build-console-image.sh
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build console container image with SBOM and optional attestations
|
||||
# Usage: ./build-console-image.sh [tag] [registry]
|
||||
# Example: ./build-console-image.sh 2025.10.0-edge ghcr.io/stellaops
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
TAG="${1:-$(date +%Y%m%dT%H%M%S)}"
|
||||
REGISTRY="${2:-registry.stella-ops.org/stellaops}"
|
||||
IMAGE_NAME="console"
|
||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
# Freeze timestamps for reproducibility
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1704067200}
|
||||
|
||||
echo "==> Building console image: ${FULL_IMAGE}"
|
||||
|
||||
# Build using the existing Dockerfile.console
|
||||
docker build \
|
||||
--file "${REPO_ROOT}/ops/devops/docker/Dockerfile.console" \
|
||||
--build-arg APP_DIR=src/Web/StellaOps.Web \
|
||||
--build-arg APP_PORT=8080 \
|
||||
--tag "${FULL_IMAGE}" \
|
||||
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--label "org.opencontainers.image.revision=$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || echo 'unknown')" \
|
||||
--label "org.opencontainers.image.source=https://github.com/stellaops/stellaops" \
|
||||
--label "org.opencontainers.image.title=StellaOps Console" \
|
||||
--label "org.opencontainers.image.description=StellaOps Angular Console (non-root nginx)" \
|
||||
"${REPO_ROOT}"
|
||||
|
||||
# Get digest
|
||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "${FULL_IMAGE}" 2>/dev/null || echo "${FULL_IMAGE}")
|
||||
|
||||
echo "==> Image built: ${FULL_IMAGE}"
|
||||
echo "==> Digest: ${DIGEST}"
|
||||
|
||||
# Output metadata for CI
|
||||
mkdir -p "${SCRIPT_DIR}/../artifacts/console"
|
||||
cat > "${SCRIPT_DIR}/../artifacts/console/build-metadata.json" <<EOF
|
||||
{
|
||||
"image": "${FULL_IMAGE}",
|
||||
"digest": "${DIGEST}",
|
||||
"tag": "${TAG}",
|
||||
"registry": "${REGISTRY}",
|
||||
"buildTime": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"gitCommit": "$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || echo 'unknown')",
|
||||
"sourceDateEpoch": "${SOURCE_DATE_EPOCH}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> Build metadata written to ops/devops/artifacts/console/build-metadata.json"
|
||||
|
||||
# Generate SBOM if syft is available
|
||||
if command -v syft &>/dev/null; then
|
||||
echo "==> Generating SBOM..."
|
||||
syft "${FULL_IMAGE}" -o spdx-json > "${SCRIPT_DIR}/../artifacts/console/console.spdx.json"
|
||||
syft "${FULL_IMAGE}" -o cyclonedx-json > "${SCRIPT_DIR}/../artifacts/console/console.cdx.json"
|
||||
echo "==> SBOMs written to ops/devops/artifacts/console/"
|
||||
else
|
||||
echo "==> Skipping SBOM generation (syft not found)"
|
||||
fi
|
||||
|
||||
# Sign and attest if cosign is available and key is set
|
||||
if command -v cosign &>/dev/null; then
|
||||
if [[ -n "${COSIGN_KEY:-}" ]]; then
|
||||
echo "==> Signing image with cosign..."
|
||||
cosign sign --key "${COSIGN_KEY}" "${FULL_IMAGE}"
|
||||
|
||||
if [[ -f "${SCRIPT_DIR}/../artifacts/console/console.spdx.json" ]]; then
|
||||
echo "==> Attesting SBOM..."
|
||||
cosign attest --predicate "${SCRIPT_DIR}/../artifacts/console/console.spdx.json" \
|
||||
--type spdx --key "${COSIGN_KEY}" "${FULL_IMAGE}"
|
||||
fi
|
||||
echo "==> Image signed and attested"
|
||||
else
|
||||
echo "==> Skipping signing (COSIGN_KEY not set)"
|
||||
fi
|
||||
else
|
||||
echo "==> Skipping signing (cosign not found)"
|
||||
fi
|
||||
|
||||
echo "==> Console image build complete"
|
||||
echo " Image: ${FULL_IMAGE}"
|
||||
44
devops/services/console/build-runner-image-ci.sh
Executable file
44
devops/services/console/build-runner-image-ci.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# CI-friendly wrapper to build the console runner image with baked npm/Playwright caches
|
||||
# and emit a tarball + metadata for offline distribution.
|
||||
#
|
||||
# Inputs (env):
|
||||
# RUN_ID : unique run identifier (default: $GITHUB_RUN_ID or UTC timestamp)
|
||||
# IMAGE_TAG : optional override of image tag (default: stellaops/console-runner:offline-$RUN_ID)
|
||||
# OUTPUT_TAR : optional override of tarball path (default: ops/devops/artifacts/console-runner/console-runner-$RUN_ID.tar)
|
||||
# APP_DIR : optional override of app directory (default: src/Web/StellaOps.Web)
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
RUN_ID="${RUN_ID:-${GITHUB_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}}"
|
||||
APP_DIR="${APP_DIR:-src/Web/StellaOps.Web}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-stellaops/console-runner:offline-$RUN_ID}"
|
||||
OUTPUT_TAR="${OUTPUT_TAR:-$ROOT/ops/devops/artifacts/console-runner/console-runner-$RUN_ID.tar}"
|
||||
META_DIR="$(dirname "$OUTPUT_TAR")"
|
||||
META_JSON="$META_DIR/console-runner-$RUN_ID.json"
|
||||
|
||||
mkdir -p "$META_DIR"
|
||||
|
||||
IMAGE_TAG="$IMAGE_TAG" OUTPUT_TAR="$OUTPUT_TAR" APP_DIR="$APP_DIR" "$ROOT/ops/devops/console/build-runner-image.sh"
|
||||
|
||||
digest="$(docker image inspect --format='{{index .RepoDigests 0}}' "$IMAGE_TAG" || true)"
|
||||
id="$(docker image inspect --format='{{.Id}}' "$IMAGE_TAG" || true)"
|
||||
|
||||
cat > "$META_JSON" <<EOF
|
||||
{
|
||||
"run_id": "$RUN_ID",
|
||||
"image_tag": "$IMAGE_TAG",
|
||||
"image_id": "$id",
|
||||
"repo_digest": "$digest",
|
||||
"output_tar": "$(python - <<PY
|
||||
import os, sys
|
||||
print(os.path.relpath("$OUTPUT_TAR","$ROOT"))
|
||||
PY
|
||||
)"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Built $IMAGE_TAG"
|
||||
echo "Saved tarball: $OUTPUT_TAR"
|
||||
echo "Metadata: $META_JSON"
|
||||
29
devops/services/console/build-runner-image.sh
Executable file
29
devops/services/console/build-runner-image.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Builds the offline console CI runner image with baked npm/Playwright caches.
|
||||
# IMAGE_TAG: docker tag to produce (default: stellaops/console-runner:offline)
|
||||
# OUTPUT_TAR: optional path to save the image tarball for airgap use.
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
IMAGE_TAG=${IMAGE_TAG:-stellaops/console-runner:offline}
|
||||
DOCKERFILE=${DOCKERFILE:-ops/devops/console/Dockerfile.runner}
|
||||
APP_DIR=${APP_DIR:-src/Web/StellaOps.Web}
|
||||
OUTPUT_TAR=${OUTPUT_TAR:-}
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker not found; install Docker/Podman before building the runner image." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker build -f "$ROOT/$DOCKERFILE" --build-arg APP_DIR="$APP_DIR" -t "$IMAGE_TAG" "$ROOT"
|
||||
|
||||
if [[ -n "$OUTPUT_TAR" ]]; then
|
||||
mkdir -p "$(dirname "$OUTPUT_TAR")"
|
||||
docker save "$IMAGE_TAG" -o "$OUTPUT_TAR"
|
||||
fi
|
||||
|
||||
echo "Runner image built: $IMAGE_TAG"
|
||||
if [[ -n "$OUTPUT_TAR" ]]; then
|
||||
echo "Saved tarball: $OUTPUT_TAR"
|
||||
fi
|
||||
131
devops/services/console/package-offline-bundle.sh
Normal file
131
devops/services/console/package-offline-bundle.sh
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# Package console for offline/airgap deployment
|
||||
# Usage: ./package-offline-bundle.sh [image] [output-dir]
|
||||
# Example: ./package-offline-bundle.sh registry.stella-ops.org/stellaops/console:2025.10.0 ./offline-bundle
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
IMAGE="${1:-registry.stella-ops.org/stellaops/console:latest}"
|
||||
OUTPUT_DIR="${2:-${SCRIPT_DIR}/../artifacts/console/offline-bundle}"
|
||||
BUNDLE_NAME="console-offline-$(date +%Y%m%dT%H%M%S)"
|
||||
|
||||
# Freeze timestamps
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1704067200}
|
||||
|
||||
echo "==> Creating offline bundle for: ${IMAGE}"
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
# Save image as tarball
|
||||
IMAGE_TAR="${OUTPUT_DIR}/${BUNDLE_NAME}.tar"
|
||||
echo "==> Saving image to ${IMAGE_TAR}..."
|
||||
docker save "${IMAGE}" -o "${IMAGE_TAR}"
|
||||
|
||||
# Calculate checksums
|
||||
echo "==> Generating checksums..."
|
||||
cd "${OUTPUT_DIR}"
|
||||
sha256sum "${BUNDLE_NAME}.tar" > "${BUNDLE_NAME}.tar.sha256"
|
||||
|
||||
# Copy Helm values
|
||||
echo "==> Including Helm values overlay..."
|
||||
cp "${REPO_ROOT}/deploy/helm/stellaops/values-console.yaml" "${OUTPUT_DIR}/"
|
||||
|
||||
# Copy Dockerfile for reference
|
||||
cp "${REPO_ROOT}/ops/devops/docker/Dockerfile.console" "${OUTPUT_DIR}/"
|
||||
|
||||
# Generate SBOMs if syft available
|
||||
if command -v syft &>/dev/null; then
|
||||
echo "==> Generating SBOMs..."
|
||||
syft "${IMAGE}" -o spdx-json > "${OUTPUT_DIR}/${BUNDLE_NAME}.spdx.json"
|
||||
syft "${IMAGE}" -o cyclonedx-json > "${OUTPUT_DIR}/${BUNDLE_NAME}.cdx.json"
|
||||
fi
|
||||
|
||||
# Create manifest
|
||||
cat > "${OUTPUT_DIR}/manifest.json" <<EOF
|
||||
{
|
||||
"bundle": "${BUNDLE_NAME}",
|
||||
"image": "${IMAGE}",
|
||||
"imageTarball": "${BUNDLE_NAME}.tar",
|
||||
"checksumFile": "${BUNDLE_NAME}.tar.sha256",
|
||||
"helmValues": "values-console.yaml",
|
||||
"dockerfile": "Dockerfile.console",
|
||||
"sbom": {
|
||||
"spdx": "${BUNDLE_NAME}.spdx.json",
|
||||
"cyclonedx": "${BUNDLE_NAME}.cdx.json"
|
||||
},
|
||||
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"sourceDateEpoch": "${SOURCE_DATE_EPOCH}"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create load script
|
||||
cat > "${OUTPUT_DIR}/load.sh" <<'LOAD'
|
||||
#!/usr/bin/env bash
|
||||
# Load console image into local Docker daemon
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MANIFEST="${SCRIPT_DIR}/manifest.json"
|
||||
|
||||
if [[ ! -f "${MANIFEST}" ]]; then
|
||||
echo "ERROR: manifest.json not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARBALL=$(jq -r '.imageTarball' "${MANIFEST}")
|
||||
CHECKSUM_FILE=$(jq -r '.checksumFile' "${MANIFEST}")
|
||||
|
||||
echo "==> Verifying checksum..."
|
||||
cd "${SCRIPT_DIR}"
|
||||
sha256sum -c "${CHECKSUM_FILE}"
|
||||
|
||||
echo "==> Loading image..."
|
||||
docker load -i "${TARBALL}"
|
||||
|
||||
IMAGE=$(jq -r '.image' "${MANIFEST}")
|
||||
echo "==> Image loaded: ${IMAGE}"
|
||||
LOAD
|
||||
chmod +x "${OUTPUT_DIR}/load.sh"
|
||||
|
||||
# Create README
|
||||
cat > "${OUTPUT_DIR}/README.md" <<EOF
|
||||
# Console Offline Bundle
|
||||
|
||||
This bundle contains the StellaOps Console container image and deployment assets
|
||||
for air-gapped environments.
|
||||
|
||||
## Contents
|
||||
|
||||
- \`${BUNDLE_NAME}.tar\` - Docker image tarball
|
||||
- \`${BUNDLE_NAME}.tar.sha256\` - SHA-256 checksum
|
||||
- \`values-console.yaml\` - Helm values overlay
|
||||
- \`Dockerfile.console\` - Reference Dockerfile
|
||||
- \`${BUNDLE_NAME}.spdx.json\` - SPDX SBOM (if generated)
|
||||
- \`${BUNDLE_NAME}.cdx.json\` - CycloneDX SBOM (if generated)
|
||||
- \`manifest.json\` - Bundle manifest
|
||||
- \`load.sh\` - Image load helper script
|
||||
|
||||
## Usage
|
||||
|
||||
1. Transfer this bundle to the air-gapped environment
|
||||
2. Verify checksums: \`sha256sum -c ${BUNDLE_NAME}.tar.sha256\`
|
||||
3. Load image: \`./load.sh\` or \`docker load -i ${BUNDLE_NAME}.tar\`
|
||||
4. Deploy with Helm: \`helm install stellaops ../stellaops -f values-console.yaml\`
|
||||
|
||||
## Image Details
|
||||
|
||||
- Image: \`${IMAGE}\`
|
||||
- Created: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
- Non-root user: UID 101 (nginx-unprivileged)
|
||||
- Port: 8080
|
||||
|
||||
## Verification
|
||||
|
||||
The image runs as non-root and supports read-only root filesystem.
|
||||
Enable \`readOnlyRootFilesystem: true\` in your security context.
|
||||
EOF
|
||||
|
||||
echo "==> Offline bundle created at: ${OUTPUT_DIR}"
|
||||
echo "==> Contents:"
|
||||
ls -la "${OUTPUT_DIR}"
|
||||
22
devops/services/console/seed_playwright.sh
Normal file
22
devops/services/console/seed_playwright.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Seeds the Playwright browser cache for offline console CI runs.
|
||||
# Run on a connected runner once, then bake ~/.cache/ms-playwright into the runner image.
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
pushd "$ROOT/src/Web" >/dev/null
|
||||
|
||||
if ! command -v npx >/dev/null; then
|
||||
echo "npx not found; install Node.js 20+ first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Playwright Chromium to ~/.cache/ms-playwright ..."
|
||||
PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH:-~/.cache/ms-playwright}
|
||||
export PLAYWRIGHT_BROWSERS_PATH
|
||||
|
||||
npx playwright install chromium --with-deps
|
||||
|
||||
echo "Done. Cache directory: $PLAYWRIGHT_BROWSERS_PATH"
|
||||
popd >/dev/null
|
||||
13
devops/services/crypto/sim-crypto-service/Dockerfile
Normal file
13
devops/services/crypto/sim-crypto-service/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY SimCryptoService.csproj .
|
||||
RUN dotnet restore
|
||||
COPY . .
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8080
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
ENTRYPOINT ["dotnet", "SimCryptoService.dll"]
|
||||
128
devops/services/crypto/sim-crypto-service/Program.cs
Normal file
128
devops/services/crypto/sim-crypto-service/Program.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
// Static key material for simulations (not for production use).
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var ecdsaPublic = ecdsa.ExportSubjectPublicKeyInfo();
|
||||
|
||||
byte[] Sign(string message, string algorithm)
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(message);
|
||||
var lower = algorithm.Trim().ToLowerInvariant();
|
||||
var upper = algorithm.Trim().ToUpperInvariant();
|
||||
|
||||
if (lower is "pq.dilithium3" or "pq.falcon512" or "pq.sim" || upper is "DILITHIUM3" or "FALCON512")
|
||||
{
|
||||
return HMACSHA256.HashData(Encoding.UTF8.GetBytes("pq-sim-key"), data);
|
||||
}
|
||||
|
||||
if (lower is "ru.magma.sim" or "ru.kuznyechik.sim" || upper is "GOST12-256" or "GOST12-512")
|
||||
{
|
||||
return HMACSHA256.HashData(Encoding.UTF8.GetBytes("gost-sim-key"), data);
|
||||
}
|
||||
|
||||
if (lower is "sm.sim" or "sm2.sim" || upper is "SM2")
|
||||
{
|
||||
return HMACSHA256.HashData(Encoding.UTF8.GetBytes("sm-sim-key"), data);
|
||||
}
|
||||
|
||||
return ecdsa.SignData(data, HashAlgorithmName.SHA256);
|
||||
}
|
||||
|
||||
bool Verify(string message, string algorithm, byte[] signature)
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(message);
|
||||
var lower = algorithm.Trim().ToLowerInvariant();
|
||||
var upper = algorithm.Trim().ToUpperInvariant();
|
||||
|
||||
if (lower is "pq.dilithium3" or "pq.falcon512" or "pq.sim" || upper is "DILITHIUM3" or "FALCON512")
|
||||
{
|
||||
return CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Encoding.UTF8.GetBytes("pq-sim-key"), data), signature);
|
||||
}
|
||||
|
||||
if (lower is "ru.magma.sim" or "ru.kuznyechik.sim" || upper is "GOST12-256" or "GOST12-512")
|
||||
{
|
||||
return CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Encoding.UTF8.GetBytes("gost-sim-key"), data), signature);
|
||||
}
|
||||
|
||||
if (lower is "sm.sim" or "sm2.sim" || upper is "SM2")
|
||||
{
|
||||
return CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Encoding.UTF8.GetBytes("sm-sim-key"), data), signature);
|
||||
}
|
||||
|
||||
return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256);
|
||||
}
|
||||
|
||||
app.MapPost("/sign", (SignRequest request) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Algorithm) || string.IsNullOrWhiteSpace(request.Message))
|
||||
{
|
||||
return Results.BadRequest("Algorithm and message are required.");
|
||||
}
|
||||
|
||||
var sig = Sign(request.Message, request.Algorithm);
|
||||
return Results.Json(new SignResponse(Convert.ToBase64String(sig), request.Algorithm));
|
||||
});
|
||||
|
||||
app.MapPost("/verify", (VerifyRequest request) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Algorithm) || string.IsNullOrWhiteSpace(request.Message) || string.IsNullOrWhiteSpace(request.SignatureBase64))
|
||||
{
|
||||
return Results.BadRequest("Algorithm, message, and signature are required.");
|
||||
}
|
||||
|
||||
var sig = Convert.FromBase64String(request.SignatureBase64);
|
||||
var ok = Verify(request.Message, request.Algorithm, sig);
|
||||
return Results.Json(new VerifyResponse(ok, request.Algorithm));
|
||||
});
|
||||
|
||||
app.MapGet("/keys", () =>
|
||||
{
|
||||
return Results.Json(new KeysResponse(
|
||||
Convert.ToBase64String(ecdsaPublic),
|
||||
"nistp256",
|
||||
new[]
|
||||
{
|
||||
"pq.sim",
|
||||
"DILITHIUM3",
|
||||
"FALCON512",
|
||||
"ru.magma.sim",
|
||||
"ru.kuznyechik.sim",
|
||||
"GOST12-256",
|
||||
"GOST12-512",
|
||||
"sm.sim",
|
||||
"SM2",
|
||||
"fips.sim",
|
||||
"eidas.sim",
|
||||
"kcmvp.sim",
|
||||
"world.sim"
|
||||
}));
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record KeysResponse(
|
||||
[property: JsonPropertyName("public_key_b64")] string PublicKeyBase64,
|
||||
[property: JsonPropertyName("curve")] string Curve,
|
||||
[property: JsonPropertyName("simulated_providers")] IEnumerable<string> Providers);
|
||||
32
devops/services/crypto/sim-crypto-service/README.md
Normal file
32
devops/services/crypto/sim-crypto-service/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Sim Crypto Service · 2025-12-11
|
||||
|
||||
Minimal HTTP service to simulate sovereign crypto providers when licensed hardware or certified modules are unavailable.
|
||||
|
||||
## Endpoints
|
||||
- `POST /sign` — body: `{"message":"<string>","algorithm":"<id>"}`; returns `{"signature_b64":"...","algorithm":"<id>"}`.
|
||||
- `POST /verify` — body: `{"message":"<string>","algorithm":"<id>","signature_b64":"..."}`; returns `{"ok":true/false,"algorithm":"<id>"}`.
|
||||
- `GET /keys` — returns public key info for simulated providers.
|
||||
|
||||
## Supported simulated provider IDs
|
||||
- GOST: `GOST12-256`, `GOST12-512`, `ru.magma.sim`, `ru.kuznyechik.sim` — deterministic HMAC-SHA256.
|
||||
- SM: `SM2`, `sm.sim`, `sm2.sim` — deterministic HMAC-SHA256.
|
||||
- PQ: `DILITHIUM3`, `FALCON512`, `pq.sim` — deterministic HMAC-SHA256.
|
||||
- FIPS/eIDAS/KCMVP/world: `ES256`, `ES384`, `ES512`, `fips.sim`, `eidas.sim`, `kcmvp.sim`, `world.sim` — ECDSA P-256 with a static key.
|
||||
|
||||
## Build & run
|
||||
```bash
|
||||
dotnet run -c Release --project ops/crypto/sim-crypto-service/SimCryptoService.csproj
|
||||
# or
|
||||
docker build -t sim-crypto -f ops/crypto/sim-crypto-service/Dockerfile ops/crypto/sim-crypto-service
|
||||
docker run --rm -p 8080:8080 sim-crypto
|
||||
```
|
||||
|
||||
## Wiring
|
||||
- Set `STELLAOPS_CRYPTO_ENABLE_SIM=1` to append `sim.crypto.remote` to the registry preference order.
|
||||
- Point the provider at the service: `STELLAOPS_CRYPTO_SIM_URL=http://localhost:8080` (or bind `StellaOps:Crypto:Sim:BaseAddress` in config).
|
||||
- `SimRemoteProviderOptions.Algorithms` already includes the IDs above; extend if you need extra aliases.
|
||||
|
||||
## Notes
|
||||
- Replaces the legacy SM-only simulator; use this unified service for SM, PQ, GOST, and FIPS/eIDAS/KCMVP placeholders.
|
||||
- Deterministic HMAC for SM/PQ/GOST; static ECDSA key for the rest. Not for production use.
|
||||
- No licensed binaries are shipped; everything is BCL-only.
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AssetTargetFallback></AssetTargetFallback>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
96
devops/services/crypto/sim-crypto-smoke/Program.cs
Normal file
96
devops/services/crypto/sim-crypto-smoke/Program.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var baseUrl = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_SIM_URL") ?? "http://localhost:8080";
|
||||
var profile = (Environment.GetEnvironmentVariable("SIM_PROFILE") ?? "sm").ToLowerInvariant();
|
||||
var algList = Environment.GetEnvironmentVariable("SIM_ALGORITHMS")?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
?? profile switch
|
||||
{
|
||||
"ru-free" or "ru-paid" or "gost" or "ru" => new[] { "GOST12-256", "ru.magma.sim", "ru.kuznyechik.sim" },
|
||||
"sm" or "cn" => new[] { "SM2" },
|
||||
"eidas" => new[] { "ES256" },
|
||||
"fips" => new[] { "ES256" },
|
||||
"kcmvp" => new[] { "ES256" },
|
||||
"pq" => new[] { "pq.sim", "DILITHIUM3", "FALCON512" },
|
||||
_ => new[] { "ES256", "SM2", "pq.sim" }
|
||||
};
|
||||
var message = Environment.GetEnvironmentVariable("SIM_MESSAGE") ?? "stellaops-sim-smoke";
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
|
||||
|
||||
static async Task<(bool Ok, string Error)> SignAndVerify(HttpClient client, string algorithm, string message, CancellationToken ct)
|
||||
{
|
||||
var signPayload = new SignRequest(message, algorithm);
|
||||
var signResponse = await client.PostAsJsonAsync("/sign", signPayload, ct).ConfigureAwait(false);
|
||||
if (!signResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"sign failed: {(int)signResponse.StatusCode} {signResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var signResult = await signResponse.Content.ReadFromJsonAsync<SignResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (signResult is null || string.IsNullOrWhiteSpace(signResult.SignatureBase64))
|
||||
{
|
||||
return (false, "sign returned empty payload");
|
||||
}
|
||||
|
||||
var verifyPayload = new VerifyRequest(message, signResult.SignatureBase64, algorithm);
|
||||
var verifyResponse = await client.PostAsJsonAsync("/verify", verifyPayload, ct).ConfigureAwait(false);
|
||||
if (!verifyResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"verify failed: {(int)verifyResponse.StatusCode} {verifyResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var verifyResult = await verifyResponse.Content.ReadFromJsonAsync<VerifyResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (verifyResult?.Ok is not true)
|
||||
{
|
||||
return (false, "verify returned false");
|
||||
}
|
||||
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
var failures = new List<string>();
|
||||
|
||||
foreach (var alg in algList)
|
||||
{
|
||||
var (ok, error) = await SignAndVerify(client, alg, message, cts.Token);
|
||||
if (!ok)
|
||||
{
|
||||
failures.Add($"{alg}: {error}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[ok] {alg} via {baseUrl}");
|
||||
}
|
||||
|
||||
if (failures.Count > 0)
|
||||
{
|
||||
Console.Error.WriteLine("Simulation smoke failed:");
|
||||
foreach (var f in failures)
|
||||
{
|
||||
Console.Error.WriteLine($" - {f}");
|
||||
}
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
Console.WriteLine("Simulation smoke passed.");
|
||||
|
||||
internal sealed record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AssetTargetFallback></AssetTargetFallback>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
185
devops/services/cryptopro/install-linux-csp.sh
Normal file
185
devops/services/cryptopro/install-linux-csp.sh
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/bin/bash
|
||||
# CryptoPro CSP 5.0 R3 Linux installer (deb packages)
|
||||
# Uses locally provided .deb packages under /opt/cryptopro/downloads (host volume).
|
||||
# No Wine dependency. Runs offline against the supplied packages only.
|
||||
#
|
||||
# Env:
|
||||
# CRYPTOPRO_INSTALL_FROM Path to folder with .deb packages (default /opt/cryptopro/downloads)
|
||||
# CRYPTOPRO_ACCEPT_EULA Must be 1 to proceed (default 0 -> hard stop with warning)
|
||||
# CRYPTOPRO_SKIP_APT_FIX Set to 1 to skip `apt-get -f install` (offline strict)
|
||||
# CRYPTOPRO_PACKAGE_FILTER Optional glob (e.g., "cprocsp*amd64.deb") to narrow selection
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 success; 1 missing dir/files; 2 incompatible arch; 3 EULA not accepted.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_FROM="${CRYPTOPRO_INSTALL_FROM:-/opt/cryptopro/downloads}"
|
||||
PACKAGE_FILTER="${CRYPTOPRO_PACKAGE_FILTER:-*.deb}"
|
||||
SKIP_APT_FIX="${CRYPTOPRO_SKIP_APT_FIX:-0}"
|
||||
STAGING_DIR="/tmp/cryptopro-debs"
|
||||
MINIMAL="${CRYPTOPRO_MINIMAL:-1}"
|
||||
INCLUDE_PLUGIN="${CRYPTOPRO_INCLUDE_PLUGIN:-0}"
|
||||
|
||||
arch_from_uname() {
|
||||
local raw
|
||||
raw="$(uname -m)"
|
||||
case "${raw}" in
|
||||
x86_64) echo "amd64" ;;
|
||||
aarch64) echo "arm64" ;;
|
||||
arm64) echo "arm64" ;;
|
||||
i386|i686) echo "i386" ;;
|
||||
*) echo "${raw}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
HOST_ARCH="$(dpkg --print-architecture 2>/dev/null || arch_from_uname)"
|
||||
|
||||
log() {
|
||||
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [cryptopro-install] $*"
|
||||
}
|
||||
|
||||
log_err() {
|
||||
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [cryptopro-install] [ERROR] $*" >&2
|
||||
}
|
||||
|
||||
require_eula() {
|
||||
if [[ "${CRYPTOPRO_ACCEPT_EULA:-0}" != "1" ]]; then
|
||||
log_err "License not accepted. Set CRYPTOPRO_ACCEPT_EULA=1 only if you hold a valid CryptoPro license for these binaries and agree to the vendor EULA."
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
maybe_extract_bundle() {
|
||||
# Prefer a bundle that matches host arch in filename, otherwise first *.tgz
|
||||
mapfile -t TGZ < <(find "${INSTALL_FROM}" -maxdepth 1 -type f -name "*.tgz" -print 2>/dev/null | sort)
|
||||
if [[ ${#TGZ[@]} -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
local chosen=""
|
||||
for candidate in "${TGZ[@]}"; do
|
||||
if [[ "${candidate}" == *"${HOST_ARCH}"* ]]; then
|
||||
chosen="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "${chosen}" ]]; then
|
||||
chosen="${TGZ[0]}"
|
||||
fi
|
||||
log "Extracting bundle ${chosen} into ${STAGING_DIR}"
|
||||
rm -rf "${STAGING_DIR}"
|
||||
mkdir -p "${STAGING_DIR}"
|
||||
tar -xf "${chosen}" -C "${STAGING_DIR}"
|
||||
# If bundle contains a single subfolder, use it as install root
|
||||
local subdir
|
||||
subdir="$(find "${STAGING_DIR}" -maxdepth 1 -type d ! -path "${STAGING_DIR}" | head -n1)"
|
||||
if [[ -n "${subdir}" ]]; then
|
||||
INSTALL_FROM="${subdir}"
|
||||
else
|
||||
INSTALL_FROM="${STAGING_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
gather_packages() {
|
||||
if [[ ! -d "${INSTALL_FROM}" ]]; then
|
||||
log_err "Package directory not found: ${INSTALL_FROM}"
|
||||
exit 1
|
||||
fi
|
||||
maybe_extract_bundle
|
||||
mapfile -t PKGS < <(find "${INSTALL_FROM}" -maxdepth 2 -type f -name "${PACKAGE_FILTER}" -print 2>/dev/null | sort)
|
||||
if [[ ${#PKGS[@]} -eq 0 ]]; then
|
||||
log_err "No .deb packages found in ${INSTALL_FROM} (filter=${PACKAGE_FILTER})"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
apply_minimal_filter() {
|
||||
if [[ "${MINIMAL}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
local -a keep_exact=(
|
||||
"lsb-cprocsp-base"
|
||||
"lsb-cprocsp-ca-certs"
|
||||
"lsb-cprocsp-capilite-64"
|
||||
"lsb-cprocsp-kc1-64"
|
||||
"lsb-cprocsp-pkcs11-64"
|
||||
"lsb-cprocsp-rdr-64"
|
||||
"cprocsp-curl-64"
|
||||
"cprocsp-pki-cades-64"
|
||||
"cprocsp-compat-debian"
|
||||
)
|
||||
if [[ "${INCLUDE_PLUGIN}" == "1" ]]; then
|
||||
keep_exact+=("cprocsp-pki-plugin-64" "cprocsp-rdr-gui-gtk-64")
|
||||
fi
|
||||
local -a filtered=()
|
||||
for pkg in "${PKGS[@]}"; do
|
||||
local name
|
||||
name="$(dpkg-deb -f "${pkg}" Package 2>/dev/null || basename "${pkg}")"
|
||||
for wanted in "${keep_exact[@]}"; do
|
||||
if [[ "${name}" == "${wanted}" ]]; then
|
||||
filtered+=("${pkg}")
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [[ ${#filtered[@]} -gt 0 ]]; then
|
||||
log "Applying minimal package set (CRYPTOPRO_MINIMAL=1); kept ${#filtered[@]} of ${#PKGS[@]}"
|
||||
PKGS=("${filtered[@]}")
|
||||
else
|
||||
log "Minimal filter yielded no matches; using full package set"
|
||||
fi
|
||||
}
|
||||
|
||||
filter_by_arch() {
|
||||
FILTERED=()
|
||||
for pkg in "${PKGS[@]}"; do
|
||||
local pkg_arch
|
||||
pkg_arch="$(dpkg-deb -f "${pkg}" Architecture 2>/dev/null || echo "unknown")"
|
||||
if [[ "${pkg_arch}" == "all" || "${pkg_arch}" == "${HOST_ARCH}" ]]; then
|
||||
FILTERED+=("${pkg}")
|
||||
else
|
||||
log "Skipping ${pkg} (arch=${pkg_arch}, host=${HOST_ARCH})"
|
||||
fi
|
||||
done
|
||||
if [[ ${#FILTERED[@]} -eq 0 ]]; then
|
||||
log_err "No packages match host architecture ${HOST_ARCH}"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
print_matrix() {
|
||||
log "Discovered packages (arch filter: host=${HOST_ARCH}):"
|
||||
for pkg in "${FILTERED[@]}"; do
|
||||
local name ver arch
|
||||
name="$(dpkg-deb -f "${pkg}" Package 2>/dev/null || basename "${pkg}")"
|
||||
ver="$(dpkg-deb -f "${pkg}" Version 2>/dev/null || echo "unknown")"
|
||||
arch="$(dpkg-deb -f "${pkg}" Architecture 2>/dev/null || echo "unknown")"
|
||||
echo " - ${name} ${ver} (${arch}) <- ${pkg}"
|
||||
done
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
log "Installing ${#FILTERED[@]} package(s) from ${INSTALL_FROM}"
|
||||
if ! dpkg -i "${FILTERED[@]}"; then
|
||||
if [[ "${SKIP_APT_FIX}" == "1" ]]; then
|
||||
log_err "dpkg reported errors and CRYPTOPRO_SKIP_APT_FIX=1; aborting."
|
||||
exit 1
|
||||
fi
|
||||
log "Resolving dependencies with apt-get -f install (may require network if deps missing locally)"
|
||||
apt-get update >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y -f install
|
||||
fi
|
||||
log "CryptoPro packages installed. Verify with: dpkg -l | grep cprocsp"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_eula
|
||||
gather_packages
|
||||
apply_minimal_filter
|
||||
filter_by_arch
|
||||
print_matrix
|
||||
install_packages
|
||||
log "Installation finished. For headless/server use on Ubuntu 22.04 (amd64), the 'linux-amd64_deb.tgz' bundle is preferred and auto-selected."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
36
devops/services/cryptopro/linux-csp-service/Dockerfile
Normal file
36
devops/services/cryptopro/linux-csp-service/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ops/cryptopro/linux-csp-service/CryptoProLinuxApi.csproj .
|
||||
RUN dotnet restore CryptoProLinuxApi.csproj
|
||||
COPY ops/cryptopro/linux-csp-service/ .
|
||||
RUN dotnet publish CryptoProLinuxApi.csproj -c Release -r linux-x64 --self-contained true \
|
||||
/p:PublishSingleFile=true /p:DebugType=none /p:DebugSymbols=false -o /app/publish
|
||||
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ARG CRYPTOPRO_ACCEPT_EULA=0
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
CRYPTOPRO_ACCEPT_EULA=${CRYPTOPRO_ACCEPT_EULA} \
|
||||
CRYPTOPRO_MINIMAL=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System deps for CryptoPro installer
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends tar xz-utils ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# CryptoPro packages (provided in repo) and installer
|
||||
COPY opt/cryptopro/downloads/*.tgz /opt/cryptopro/downloads/
|
||||
COPY ops/cryptopro/install-linux-csp.sh /usr/local/bin/install-linux-csp.sh
|
||||
RUN chmod +x /usr/local/bin/install-linux-csp.sh
|
||||
|
||||
# Install CryptoPro CSP (requires CRYPTOPRO_ACCEPT_EULA=1 at build/runtime)
|
||||
RUN CRYPTOPRO_ACCEPT_EULA=${CRYPTOPRO_ACCEPT_EULA} /usr/local/bin/install-linux-csp.sh
|
||||
|
||||
# Copy published .NET app
|
||||
COPY --from=build /app/publish/ /app/
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/CryptoProLinuxApi"]
|
||||
118
devops/services/cryptopro/linux-csp-service/Program.cs
Normal file
118
devops/services/cryptopro/linux-csp-service/Program.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var builder = WebApplication.CreateSlimBuilder(args);
|
||||
builder.Services.ConfigureHttpJsonOptions(opts =>
|
||||
{
|
||||
opts.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
const string CsptestPath = "/opt/cprocsp/bin/amd64/csptest";
|
||||
|
||||
app.MapGet("/health", () =>
|
||||
{
|
||||
if (!File.Exists(CsptestPath))
|
||||
{
|
||||
return Results.Problem(statusCode: 500, detail: "csptest not found; ensure CryptoPro CSP is installed");
|
||||
}
|
||||
|
||||
return Results.Ok(new { status = "ok", csptest = CsptestPath });
|
||||
});
|
||||
|
||||
app.MapGet("/license", () =>
|
||||
{
|
||||
var result = RunProcess([CsptestPath, "-keyset", "-info"], allowFailure: true);
|
||||
return Results.Json(result);
|
||||
});
|
||||
|
||||
app.MapPost("/hash", async (HashRequest request) =>
|
||||
{
|
||||
byte[] data;
|
||||
try
|
||||
{
|
||||
data = Convert.FromBase64String(request.DataBase64);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid base64" });
|
||||
}
|
||||
|
||||
var inputPath = Path.GetTempFileName();
|
||||
var outputPath = Path.GetTempFileName();
|
||||
await File.WriteAllBytesAsync(inputPath, data);
|
||||
|
||||
var result = RunProcess([CsptestPath, "-hash", "-alg", "GOST12_256", "-in", inputPath, "-out", outputPath], allowFailure: true);
|
||||
string? digestBase64 = null;
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
var digestBytes = await File.ReadAllBytesAsync(outputPath);
|
||||
digestBase64 = Convert.ToBase64String(digestBytes);
|
||||
}
|
||||
|
||||
TryDelete(inputPath);
|
||||
TryDelete(outputPath);
|
||||
|
||||
return Results.Json(new
|
||||
{
|
||||
result.ExitCode,
|
||||
result.Output,
|
||||
digest_b64 = digestBase64
|
||||
});
|
||||
});
|
||||
|
||||
app.MapPost("/keyset/init", (KeysetRequest request) =>
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(request.Name) ? "default" : request.Name!;
|
||||
var result = RunProcess([CsptestPath, "-keyset", "-newkeyset", "-container", name, "-keytype", "none"], allowFailure: true);
|
||||
return Results.Json(result);
|
||||
});
|
||||
|
||||
app.Run("http://0.0.0.0:8080");
|
||||
|
||||
static void TryDelete(string path)
|
||||
{
|
||||
try { File.Delete(path); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
static ProcessResult RunProcess(string[] args, bool allowFailure = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = args[0],
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
ArgumentList = { }
|
||||
};
|
||||
for (var i = 1; i < args.Length; i++)
|
||||
{
|
||||
psi.ArgumentList.Add(args[i]);
|
||||
}
|
||||
|
||||
using var proc = Process.Start(psi)!;
|
||||
var output = proc.StandardOutput.ReadToEnd();
|
||||
output += proc.StandardError.ReadToEnd();
|
||||
proc.WaitForExit();
|
||||
if (proc.ExitCode != 0 && !allowFailure)
|
||||
{
|
||||
throw new InvalidOperationException($"Command failed with exit {proc.ExitCode}: {output}");
|
||||
}
|
||||
return new ProcessResult(proc.ExitCode, output);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!allowFailure)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
return new ProcessResult(-1, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
sealed record HashRequest([property: JsonPropertyName("data_b64")] string DataBase64);
|
||||
sealed record KeysetRequest([property: JsonPropertyName("name")] string? Name);
|
||||
sealed record ProcessResult(int ExitCode, string Output);
|
||||
33
devops/services/cryptopro/linux-csp-service/README.md
Normal file
33
devops/services/cryptopro/linux-csp-service/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# CryptoPro Linux CSP Service (.NET minimal API)
|
||||
|
||||
Minimal HTTP wrapper around the Linux CryptoPro CSP binaries to prove installation and hash operations.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
docker build -t cryptopro-linux-csp -f ops/cryptopro/linux-csp-service/Dockerfile .
|
||||
```
|
||||
|
||||
`CRYPTOPRO_ACCEPT_EULA` defaults to `0` (build will fail); set to `1` only if you hold a valid CryptoPro license and accept the vendor EULA:
|
||||
|
||||
```bash
|
||||
docker build -t cryptopro-linux-csp \
|
||||
--build-arg CRYPTOPRO_ACCEPT_EULA=1 \
|
||||
-f ops/cryptopro/linux-csp-service/Dockerfile .
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
docker run --rm -p 18080:8080 --name cryptopro-linux-csp-test cryptopro-linux-csp
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
- `GET /health` — checks `csptest` presence.
|
||||
- `GET /license` — runs `csptest -keyset -info` (reports errors if no keyset/token present).
|
||||
- `POST /hash` with `{"data_b64":"<base64>"}` — hashes using `csptest -hash -alg GOST12_256`.
|
||||
- `POST /keyset/init` with optional `{"name":"<container>"}` — creates an empty keyset (`-keytype none`) to silence missing-container warnings.
|
||||
|
||||
Notes:
|
||||
- Uses the provided CryptoPro `.tgz` bundles under `opt/cryptopro/downloads`. Do not set `CRYPTOPRO_ACCEPT_EULA=1` unless you are licensed to use these binaries.
|
||||
- Minimal, headless install; browser/plugin packages are not included.
|
||||
21
devops/services/devportal/AGENTS.md
Normal file
21
devops/services/devportal/AGENTS.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# DevPortal Build & Offline — Agent Charter
|
||||
|
||||
## Mission
|
||||
Automate deterministic developer portal builds (online/offline), enforce accessibility/performance budgets, and publish nightly offline bundles with checksums and provenance.
|
||||
|
||||
## Scope
|
||||
- CI pipeline for `devportal` (pnpm install, lint, type-check, unit, a11y, Lighthouse perf, caching).
|
||||
- Offline/nightly build (`devportal --offline`) with artifact retention and checksum manifest.
|
||||
- Accessibility checks (axe/pa11y) and link checking for docs/content.
|
||||
- Performance budgets via Lighthouse (P95) recorded per commit.
|
||||
|
||||
## Working Agreements
|
||||
- Use pnpm with a locked store; no network during build steps beyond configured registries/mirrors.
|
||||
- Keep outputs deterministic: pinned deps, `NODE_OPTIONS=--enable-source-maps`, UTC timestamps.
|
||||
- Artifacts stored under `out/devportal/<run-id>` with `SHA256SUMS` manifest.
|
||||
- Update sprint entries when task states change; record evidence bundle paths in Execution Log.
|
||||
|
||||
## Required Reading
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/modules/devops/architecture.md`
|
||||
- `docs/modules/ui/architecture.md`
|
||||
32
devops/services/evidence-locker/alerts.yaml
Normal file
32
devops/services/evidence-locker/alerts.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
groups:
|
||||
- name: evidence-locker
|
||||
rules:
|
||||
- alert: EvidenceLockerRetentionDrift
|
||||
expr: evidence_retention_days != 180
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "Evidence locker retention drift"
|
||||
description: "Configured retention {{ $value }}d differs from target 180d."
|
||||
|
||||
- alert: EvidenceLockerWormDisabled
|
||||
expr: evidence_worm_enabled == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "WORM/immutability disabled"
|
||||
description: "Evidence locker WORM not enabled."
|
||||
|
||||
- alert: EvidenceLockerBackupLag
|
||||
expr: (time() - evidence_last_backup_seconds) > 3600
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "Evidence locker backup lag > 1h"
|
||||
description: "Last backup older than 1 hour."
|
||||
23
devops/services/evidence-locker/grafana/evidence-locker.json
Normal file
23
devops/services/evidence-locker/grafana/evidence-locker.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"title": "Evidence Locker",
|
||||
"time": { "from": "now-24h", "to": "now" },
|
||||
"panels": [
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "WORM enabled",
|
||||
"targets": [{ "expr": "evidence_worm_enabled" }]
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Retention days",
|
||||
"targets": [{ "expr": "evidence_retention_days" }]
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Backup lag (seconds)",
|
||||
"targets": [{ "expr": "time() - evidence_last_backup_seconds" }]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"version": 1
|
||||
}
|
||||
21
devops/services/export/minio-compose.yml
Normal file
21
devops/services/export/minio-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2024-10-08T09-56-18Z
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: exportci
|
||||
MINIO_ROOT_PASSWORD: exportci123
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
volumes:
|
||||
minio-data:
|
||||
driver: local
|
||||
23
devops/services/export/seed-minio.sh
Normal file
23
devops/services/export/seed-minio.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
MINIO_ENDPOINT=${MINIO_ENDPOINT:-http://localhost:9000}
|
||||
MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-exportci}
|
||||
MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-exportci123}
|
||||
BUCKET=${BUCKET:-export-ci}
|
||||
TMP=$(mktemp)
|
||||
cleanup(){ rm -f "$TMP"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
cat > "$TMP" <<'DATA'
|
||||
{"id":"exp-001","object":"s3://export-ci/sample-export.ndjson","status":"ready"}
|
||||
DATA
|
||||
|
||||
export AWS_ACCESS_KEY_ID="$MINIO_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$MINIO_SECRET_KEY"
|
||||
export AWS_EC2_METADATA_DISABLED=true
|
||||
|
||||
if ! aws --endpoint-url "$MINIO_ENDPOINT" s3 ls "s3://$BUCKET" >/dev/null 2>&1; then
|
||||
aws --endpoint-url "$MINIO_ENDPOINT" s3 mb "s3://$BUCKET"
|
||||
fi
|
||||
aws --endpoint-url "$MINIO_ENDPOINT" s3 cp "$TMP" "s3://$BUCKET/sample-export.ndjson"
|
||||
echo "Seeded $BUCKET/sample-export.ndjson"
|
||||
51
devops/services/export/trivy-smoke.sh
Normal file
51
devops/services/export/trivy-smoke.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Smoke tests for Trivy compatibility and OCI distribution for Export Center.
|
||||
ROOT=${ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}
|
||||
ARTifacts=${ARTifacts:-$ROOT/out/export-smoke}
|
||||
mkdir -p "$ARTifacts"
|
||||
|
||||
# 1) Trivy DB import compatibility
|
||||
TRIVY_VERSION="0.52.2"
|
||||
TRIVY_BIN="$ARTifacts/trivy"
|
||||
if [[ ! -x "$TRIVY_BIN" ]]; then
|
||||
curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" -o "$ARTifacts/trivy.tgz"
|
||||
tar -xzf "$ARTifacts/trivy.tgz" -C "$ARTifacts" trivy
|
||||
fi
|
||||
"$TRIVY_BIN" module db import --help > "$ARTifacts/trivy-import-help.txt"
|
||||
|
||||
# 2) OCI distribution check (local registry)
|
||||
REGISTRY_PORT=${REGISTRY_PORT:-5005}
|
||||
REGISTRY_DIR="$ARTifacts/registry"
|
||||
mkdir -p "$REGISTRY_DIR"
|
||||
podman run --rm -d -p "${REGISTRY_PORT}:5000" --name export-registry -v "$REGISTRY_DIR":/var/lib/registry registry:2
|
||||
trap 'podman rm -f export-registry >/dev/null 2>&1 || true' EXIT
|
||||
sleep 2
|
||||
|
||||
echo '{"schemaVersion":2,"manifests":[]}' > "$ARTifacts/empty-index.json"
|
||||
DIGEST=$(sha256sum "$ARTifacts/empty-index.json" | awk '{print $1}')
|
||||
mkdir -p "$ARTifacts/blobs/sha256"
|
||||
cp "$ARTifacts/empty-index.json" "$ARTifacts/blobs/sha256/$DIGEST"
|
||||
|
||||
# Push blob and manifest via curl
|
||||
cat > "$ARTifacts/manifest.json" <<JSON
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"size": 2,
|
||||
"digest": "sha256:d4735e3a265e16eee03f59718b9b5d03d68c8ffa19c2f8f71b66e08d6c6f2c1a"
|
||||
},
|
||||
"layers": []
|
||||
}
|
||||
JSON
|
||||
MAN_DIGEST=$(sha256sum "$ARTifacts/manifest.json" | awk '{print $1}')
|
||||
|
||||
curl -sSf -X PUT "http://localhost:${REGISTRY_PORT}/v2/export-smoke/blobs/uploads/" -H 'Content-Length: 0' -o "$ARTifacts/upload-location.txt"
|
||||
UPLOAD_URL=$(cat "$ARTifacts/upload-location.txt" | tr -d '\r\n')
|
||||
|
||||
curl -sSf -X PUT "${UPLOAD_URL}?digest=sha256:${MAN_DIGEST}" --data-binary "@$ARTifacts/manifest.json"
|
||||
|
||||
curl -sSf "http://localhost:${REGISTRY_PORT}/v2/export-smoke/manifests/sha256:${MAN_DIGEST}" -o "$ARTifacts/manifest.pull.json"
|
||||
echo "trivy smoke + oci registry ok" > "$ARTifacts/result.txt"
|
||||
42
devops/services/exporter/alerts.yaml
Normal file
42
devops/services/exporter/alerts.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
groups:
|
||||
- name: exporter
|
||||
rules:
|
||||
- alert: ExporterThroughputLow
|
||||
expr: rate(exporter_jobs_processed_total[5m]) < 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "Exporter throughput low"
|
||||
description: "Processed <1 job/s over last 5m (current {{ $value }})."
|
||||
|
||||
- alert: ExporterFailuresHigh
|
||||
expr: rate(exporter_jobs_failed_total[5m]) / rate(exporter_jobs_processed_total[5m]) > 0.02
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "Exporter failure rate >2%"
|
||||
description: "Failure rate {{ $value | humanizePercentage }} over last 5m."
|
||||
|
||||
- alert: ExporterLatencyP95High
|
||||
expr: histogram_quantile(0.95, sum(rate(exporter_job_duration_seconds_bucket[5m])) by (le)) > 3
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "Exporter job p95 latency high"
|
||||
description: "Job p95 latency {{ $value }}s over last 5m (threshold 3s)."
|
||||
|
||||
- alert: ExporterQueueDepthHigh
|
||||
expr: exporter_queue_depth > 500
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
team: devops
|
||||
annotations:
|
||||
summary: "Exporter queue depth high"
|
||||
description: "Queue depth {{ $value }} exceeds 500 for >10m."
|
||||
29
devops/services/exporter/grafana/exporter-overview.json
Normal file
29
devops/services/exporter/grafana/exporter-overview.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"title": "Exporter Overview",
|
||||
"time": { "from": "now-24h", "to": "now" },
|
||||
"panels": [
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Queue depth",
|
||||
"targets": [{ "expr": "exporter_queue_depth" }]
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Jobs processed / failed",
|
||||
"targets": [
|
||||
{ "expr": "rate(exporter_jobs_processed_total[5m])", "legendFormat": "processed" },
|
||||
{ "expr": "rate(exporter_jobs_failed_total[5m])", "legendFormat": "failed" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Job duration p50/p95",
|
||||
"targets": [
|
||||
{ "expr": "histogram_quantile(0.5, sum(rate(exporter_job_duration_seconds_bucket[5m])) by (le))", "legendFormat": "p50" },
|
||||
{ "expr": "histogram_quantile(0.95, sum(rate(exporter_job_duration_seconds_bucket[5m])) by (le))", "legendFormat": "p95" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Findings Ledger Docker Compose overlay
|
||||
# Append to or reference from your main compose file
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yaml -f ops/devops/findings-ledger/compose/docker-compose.ledger.yaml up -d
|
||||
|
||||
services:
|
||||
findings-ledger:
|
||||
image: stellaops/findings-ledger:${STELLA_VERSION:-2025.11.0}
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./env/ledger.${STELLAOPS_ENV:-dev}.env
|
||||
environment:
|
||||
ASPNETCORE_URLS: http://0.0.0.0:8080
|
||||
ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
|
||||
# Database connection (override via env file or secrets)
|
||||
# LEDGER__DB__CONNECTIONSTRING: see secrets
|
||||
# Observability
|
||||
LEDGER__OBSERVABILITY__ENABLED: "true"
|
||||
LEDGER__OBSERVABILITY__OTLPENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4317}
|
||||
# Merkle anchoring
|
||||
LEDGER__MERKLE__ANCHORINTERVAL: "00:05:00"
|
||||
LEDGER__MERKLE__EXTERNALIZE: ${LEDGER_MERKLE_EXTERNALIZE:-false}
|
||||
# Attachments
|
||||
LEDGER__ATTACHMENTS__MAXSIZEBYTES: "104857600" # 100MB
|
||||
LEDGER__ATTACHMENTS__ALLOWEGRESS: ${LEDGER_ATTACHMENTS_ALLOWEGRESS:-true}
|
||||
ports:
|
||||
- "${LEDGER_PORT:-8188}:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8080/health/ready"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
volumes:
|
||||
- ledger-data:/app/data
|
||||
- ./etc/ledger/appsettings.json:/app/appsettings.json:ro
|
||||
networks:
|
||||
- stellaops
|
||||
|
||||
# Migration job (run before starting ledger)
|
||||
findings-ledger-migrations:
|
||||
image: stellaops/findings-ledger-migrations:${STELLA_VERSION:-2025.11.0}
|
||||
command: ["--connection", "${LEDGER__DB__CONNECTIONSTRING}"]
|
||||
env_file:
|
||||
- ./env/ledger.${STELLAOPS_ENV:-dev}.env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- stellaops
|
||||
profiles:
|
||||
- migrations
|
||||
|
||||
volumes:
|
||||
ledger-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
stellaops:
|
||||
external: true
|
||||
24
devops/services/findings-ledger/compose/env/ledger.dev.env
vendored
Normal file
24
devops/services/findings-ledger/compose/env/ledger.dev.env
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Findings Ledger - Development Environment
|
||||
# Copy to ledger.local.env and customize for local dev
|
||||
|
||||
# Database connection
|
||||
LEDGER__DB__CONNECTIONSTRING=Host=postgres;Port=5432;Database=findings_ledger_dev;Username=ledger;Password=change_me_dev;
|
||||
|
||||
# Attachment encryption key (AES-256, base64 encoded)
|
||||
# Generate with: openssl rand -base64 32
|
||||
LEDGER__ATTACHMENTS__ENCRYPTIONKEY=
|
||||
|
||||
# Merkle anchor signing (optional in dev)
|
||||
LEDGER__MERKLE__SIGNINGKEY=
|
||||
|
||||
# Authority service endpoint (for JWT validation)
|
||||
LEDGER__AUTHORITY__BASEURL=http://authority:8080
|
||||
|
||||
# Logging level
|
||||
Logging__LogLevel__Default=Debug
|
||||
Logging__LogLevel__Microsoft=Information
|
||||
Logging__LogLevel__StellaOps=Debug
|
||||
|
||||
# Feature flags
|
||||
LEDGER__FEATURES__ENABLEATTACHMENTS=true
|
||||
LEDGER__FEATURES__ENABLEAUDITLOG=true
|
||||
40
devops/services/findings-ledger/compose/env/ledger.prod.env
vendored
Normal file
40
devops/services/findings-ledger/compose/env/ledger.prod.env
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Findings Ledger - Production Environment
|
||||
# Secrets should be injected from secrets manager, not committed
|
||||
|
||||
# Database connection (inject from secrets manager)
|
||||
# LEDGER__DB__CONNECTIONSTRING=
|
||||
|
||||
# Attachment encryption key (inject from secrets manager)
|
||||
# LEDGER__ATTACHMENTS__ENCRYPTIONKEY=
|
||||
|
||||
# Merkle anchor signing (inject from secrets manager)
|
||||
# LEDGER__MERKLE__SIGNINGKEY=
|
||||
|
||||
# Authority service endpoint
|
||||
LEDGER__AUTHORITY__BASEURL=http://authority:8080
|
||||
|
||||
# Logging level
|
||||
Logging__LogLevel__Default=Warning
|
||||
Logging__LogLevel__Microsoft=Warning
|
||||
Logging__LogLevel__StellaOps=Information
|
||||
|
||||
# Feature flags
|
||||
LEDGER__FEATURES__ENABLEATTACHMENTS=true
|
||||
LEDGER__FEATURES__ENABLEAUDITLOG=true
|
||||
|
||||
# Observability
|
||||
LEDGER__OBSERVABILITY__ENABLED=true
|
||||
LEDGER__OBSERVABILITY__METRICSPORT=9090
|
||||
|
||||
# Merkle anchoring
|
||||
LEDGER__MERKLE__ANCHORINTERVAL=00:05:00
|
||||
LEDGER__MERKLE__EXTERNALIZE=false
|
||||
|
||||
# Attachments
|
||||
LEDGER__ATTACHMENTS__MAXSIZEBYTES=104857600
|
||||
LEDGER__ATTACHMENTS__ALLOWEGRESS=false
|
||||
|
||||
# Air-gap staleness thresholds (seconds)
|
||||
LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD=604800
|
||||
LEDGER__AIRGAP__VEXSTALETHRESHOLD=604800
|
||||
LEDGER__AIRGAP__POLICYSTALETHRESHOLD=86400
|
||||
20
devops/services/findings-ledger/helm/Chart.yaml
Normal file
20
devops/services/findings-ledger/helm/Chart.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: v2
|
||||
name: stellaops-findings-ledger
|
||||
version: 0.1.0
|
||||
appVersion: "2025.11.0"
|
||||
description: Findings Ledger service for StellaOps platform - event-sourced findings storage with Merkle anchoring.
|
||||
type: application
|
||||
keywords:
|
||||
- findings
|
||||
- ledger
|
||||
- event-sourcing
|
||||
- merkle
|
||||
- attestation
|
||||
maintainers:
|
||||
- name: StellaOps Team
|
||||
email: platform@stellaops.io
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: "14.x"
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: postgresql.enabled
|
||||
80
devops/services/findings-ledger/helm/templates/_helpers.tpl
Normal file
80
devops/services/findings-ledger/helm/templates/_helpers.tpl
Normal file
@@ -0,0 +1,80 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "findings-ledger.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "findings-ledger.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "findings-ledger.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "findings-ledger.labels" -}}
|
||||
helm.sh/chart: {{ include "findings-ledger.chart" . }}
|
||||
{{ include "findings-ledger.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "findings-ledger.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "findings-ledger.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: ledger
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "findings-ledger.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "findings-ledger.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Database connection string - from secret or constructed
|
||||
*/}}
|
||||
{{- define "findings-ledger.databaseConnectionString" -}}
|
||||
{{- if .Values.database.connectionStringSecret }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.database.connectionStringSecret }}
|
||||
key: {{ .Values.database.connectionStringKey }}
|
||||
{{- else if .Values.postgresql.enabled }}
|
||||
value: "Host={{ .Release.Name }}-postgresql;Port=5432;Database={{ .Values.postgresql.auth.database }};Username={{ .Values.postgresql.auth.username }};Password=$(POSTGRES_PASSWORD);"
|
||||
{{- else }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secrets.name }}
|
||||
key: LEDGER__DB__CONNECTIONSTRING
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "findings-ledger.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "findings-ledger.labels" . | nindent 4 }}
|
||||
data:
|
||||
appsettings.json: |
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"StellaOps": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
122
devops/services/findings-ledger/helm/templates/deployment.yaml
Normal file
122
devops/services/findings-ledger/helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "findings-ledger.fullname" . }}
|
||||
labels:
|
||||
{{- include "findings-ledger.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "findings-ledger.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
labels:
|
||||
{{- include "findings-ledger.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "findings-ledger.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: ledger
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
{{- if .Values.observability.metricsEnabled }}
|
||||
- name: metrics
|
||||
containerPort: {{ .Values.service.metricsPort }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://0.0.0.0:{{ .Values.service.port }}"
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Production"
|
||||
# Database
|
||||
- name: LEDGER__DB__CONNECTIONSTRING
|
||||
{{- include "findings-ledger.databaseConnectionString" . | nindent 14 }}
|
||||
# Observability
|
||||
- name: LEDGER__OBSERVABILITY__ENABLED
|
||||
value: {{ .Values.observability.enabled | quote }}
|
||||
- name: LEDGER__OBSERVABILITY__OTLPENDPOINT
|
||||
value: {{ .Values.observability.otlpEndpoint | quote }}
|
||||
# Merkle anchoring
|
||||
- name: LEDGER__MERKLE__ANCHORINTERVAL
|
||||
value: {{ .Values.merkle.anchorInterval | quote }}
|
||||
- name: LEDGER__MERKLE__EXTERNALIZE
|
||||
value: {{ .Values.merkle.externalize | quote }}
|
||||
# Attachments
|
||||
- name: LEDGER__ATTACHMENTS__MAXSIZEBYTES
|
||||
value: {{ .Values.attachments.maxSizeBytes | quote }}
|
||||
- name: LEDGER__ATTACHMENTS__ALLOWEGRESS
|
||||
value: {{ .Values.attachments.allowEgress | quote }}
|
||||
- name: LEDGER__ATTACHMENTS__ENCRYPTIONKEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secrets.name }}
|
||||
key: LEDGER__ATTACHMENTS__ENCRYPTIONKEY
|
||||
# Authority
|
||||
- name: LEDGER__AUTHORITY__BASEURL
|
||||
value: {{ .Values.authority.baseUrl | quote }}
|
||||
# Air-gap thresholds
|
||||
- name: LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD
|
||||
value: {{ .Values.airgap.advisoryStaleThreshold | quote }}
|
||||
- name: LEDGER__AIRGAP__VEXSTALETHRESHOLD
|
||||
value: {{ .Values.airgap.vexStaleThreshold | quote }}
|
||||
- name: LEDGER__AIRGAP__POLICYSTALETHRESHOLD
|
||||
value: {{ .Values.airgap.policyStaleThreshold | quote }}
|
||||
# Features
|
||||
- name: LEDGER__FEATURES__ENABLEATTACHMENTS
|
||||
value: {{ .Values.features.enableAttachments | quote }}
|
||||
- name: LEDGER__FEATURES__ENABLEAUDITLOG
|
||||
value: {{ .Values.features.enableAuditLog | quote }}
|
||||
{{- with .Values.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraEnvFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{- if .Values.migrations.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "findings-ledger.fullname" . }}-migrations
|
||||
labels:
|
||||
{{- include "findings-ledger.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: migrations
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-5"
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "findings-ledger.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: migrations
|
||||
spec:
|
||||
serviceAccountName: {{ include "findings-ledger.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: migrations
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.migrations.image.repository }}:{{ .Values.migrations.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- "--connection"
|
||||
- "$(LEDGER__DB__CONNECTIONSTRING)"
|
||||
env:
|
||||
- name: LEDGER__DB__CONNECTIONSTRING
|
||||
{{- include "findings-ledger.databaseConnectionString" . | nindent 14 }}
|
||||
resources:
|
||||
{{- toYaml .Values.migrations.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
21
devops/services/findings-ledger/helm/templates/service.yaml
Normal file
21
devops/services/findings-ledger/helm/templates/service.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "findings-ledger.fullname" . }}
|
||||
labels:
|
||||
{{- include "findings-ledger.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
{{- if .Values.observability.metricsEnabled }}
|
||||
- port: {{ .Values.service.metricsPort }}
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
name: metrics
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "findings-ledger.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "findings-ledger.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "findings-ledger.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
151
devops/services/findings-ledger/helm/values.yaml
Normal file
151
devops/services/findings-ledger/helm/values.yaml
Normal file
@@ -0,0 +1,151 @@
|
||||
# Default values for stellaops-findings-ledger
|
||||
|
||||
image:
|
||||
repository: stellaops/findings-ledger
|
||||
tag: "2025.11.0"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
metricsPort: 9090
|
||||
|
||||
# Database configuration
|
||||
database:
|
||||
# External PostgreSQL connection (preferred for production)
|
||||
# Set connectionStringSecret to use existing secret
|
||||
connectionStringSecret: ""
|
||||
connectionStringKey: "LEDGER__DB__CONNECTIONSTRING"
|
||||
# Or provide connection details directly (not recommended for prod)
|
||||
host: "postgres"
|
||||
port: 5432
|
||||
database: "findings_ledger"
|
||||
username: "ledger"
|
||||
# password via secret only
|
||||
|
||||
# Built-in PostgreSQL (dev/testing only)
|
||||
postgresql:
|
||||
enabled: false
|
||||
auth:
|
||||
username: ledger
|
||||
database: findings_ledger
|
||||
|
||||
# Secrets configuration
|
||||
secrets:
|
||||
# Name of secret containing sensitive values
|
||||
name: "findings-ledger-secrets"
|
||||
# Expected keys in secret:
|
||||
# LEDGER__DB__CONNECTIONSTRING
|
||||
# LEDGER__ATTACHMENTS__ENCRYPTIONKEY
|
||||
# LEDGER__MERKLE__SIGNINGKEY (optional)
|
||||
|
||||
# Observability
|
||||
observability:
|
||||
enabled: true
|
||||
otlpEndpoint: "http://otel-collector:4317"
|
||||
metricsEnabled: true
|
||||
|
||||
# Merkle anchoring
|
||||
merkle:
|
||||
anchorInterval: "00:05:00"
|
||||
externalize: false
|
||||
# externalAnchorEndpoint: ""
|
||||
|
||||
# Attachments
|
||||
attachments:
|
||||
maxSizeBytes: 104857600 # 100MB
|
||||
allowEgress: true
|
||||
# encryptionKey via secret
|
||||
|
||||
# Air-gap configuration
|
||||
airgap:
|
||||
advisoryStaleThreshold: 604800 # 7 days
|
||||
vexStaleThreshold: 604800 # 7 days
|
||||
policyStaleThreshold: 86400 # 1 day
|
||||
|
||||
# Authority integration
|
||||
authority:
|
||||
baseUrl: "http://authority:8080"
|
||||
|
||||
# Feature flags
|
||||
features:
|
||||
enableAttachments: true
|
||||
enableAuditLog: true
|
||||
|
||||
# Resource limits
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
|
||||
# Probes
|
||||
probes:
|
||||
readiness:
|
||||
path: /health/ready
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
liveness:
|
||||
path: /health/live
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
|
||||
# Pod configuration
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
# Extra environment variables
|
||||
extraEnv: []
|
||||
# - name: CUSTOM_VAR
|
||||
# value: "value"
|
||||
|
||||
extraEnvFrom: []
|
||||
# - secretRef:
|
||||
# name: additional-secrets
|
||||
|
||||
# Migration job
|
||||
migrations:
|
||||
enabled: true
|
||||
image:
|
||||
repository: stellaops/findings-ledger-migrations
|
||||
tag: "2025.11.0"
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
|
||||
# Service account
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: ""
|
||||
annotations: {}
|
||||
|
||||
# Pod security context
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
# Container security context
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
# Ingress (optional)
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts: []
|
||||
tls: []
|
||||
158
devops/services/findings-ledger/offline-kit/README.md
Normal file
158
devops/services/findings-ledger/offline-kit/README.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Findings Ledger Offline Kit
|
||||
|
||||
This directory contains manifests and scripts for deploying Findings Ledger in air-gapped/offline environments.
|
||||
|
||||
## Contents
|
||||
|
||||
```
|
||||
offline-kit/
|
||||
├── README.md # This file
|
||||
├── manifest.yaml # Offline bundle manifest
|
||||
├── images/ # Container image tarballs (populated at build)
|
||||
│ └── .gitkeep
|
||||
├── migrations/ # Database migration scripts
|
||||
│ └── .gitkeep
|
||||
├── dashboards/ # Grafana dashboard JSON exports
|
||||
│ └── findings-ledger.json
|
||||
├── alerts/ # Prometheus alert rules
|
||||
│ └── findings-ledger-alerts.yaml
|
||||
└── scripts/
|
||||
├── import-images.sh # Load container images
|
||||
├── run-migrations.sh # Apply database migrations
|
||||
└── verify-install.sh # Post-install verification
|
||||
```
|
||||
|
||||
## Building the Offline Kit
|
||||
|
||||
Use the platform offline kit builder:
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
python ops/offline-kit/build_offline_kit.py \
|
||||
--include ledger \
|
||||
--version 2025.11.0 \
|
||||
--output dist/offline-kit-ledger-2025.11.0.tar.gz
|
||||
```
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Transfer and Extract
|
||||
|
||||
```bash
|
||||
# On air-gapped host
|
||||
tar xzf offline-kit-ledger-*.tar.gz
|
||||
cd offline-kit-ledger-*
|
||||
```
|
||||
|
||||
### 2. Load Container Images
|
||||
|
||||
```bash
|
||||
./scripts/import-images.sh
|
||||
# Loads: stellaops/findings-ledger, stellaops/findings-ledger-migrations
|
||||
```
|
||||
|
||||
### 3. Run Database Migrations
|
||||
|
||||
```bash
|
||||
export LEDGER__DB__CONNECTIONSTRING="Host=...;Database=...;..."
|
||||
./scripts/run-migrations.sh
|
||||
```
|
||||
|
||||
### 4. Deploy Service
|
||||
|
||||
Choose deployment method:
|
||||
|
||||
**Docker Compose:**
|
||||
```bash
|
||||
cp ../compose/env/ledger.prod.env ./ledger.env
|
||||
# Edit ledger.env with local values
|
||||
docker compose -f ../compose/docker-compose.ledger.yaml up -d
|
||||
```
|
||||
|
||||
**Helm:**
|
||||
```bash
|
||||
helm upgrade --install findings-ledger ../helm \
|
||||
-f values-offline.yaml \
|
||||
--set image.pullPolicy=Never
|
||||
```
|
||||
|
||||
### 5. Verify Installation
|
||||
|
||||
```bash
|
||||
./scripts/verify-install.sh
|
||||
```
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
### Sealed Mode
|
||||
|
||||
In air-gapped environments, configure:
|
||||
|
||||
```yaml
|
||||
# Disable outbound attachment egress
|
||||
LEDGER__ATTACHMENTS__ALLOWEGRESS: "false"
|
||||
|
||||
# Set appropriate staleness thresholds
|
||||
LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD: "604800" # 7 days
|
||||
LEDGER__AIRGAP__VEXSTALETHRESHOLD: "604800"
|
||||
LEDGER__AIRGAP__POLICYSTALETHRESHOLD: "86400" # 1 day
|
||||
```
|
||||
|
||||
### Merkle Anchoring
|
||||
|
||||
For offline environments without external anchoring:
|
||||
|
||||
```yaml
|
||||
LEDGER__MERKLE__EXTERNALIZE: "false"
|
||||
```
|
||||
|
||||
Keep local Merkle roots and export periodically for audit.
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
See `docs/modules/findings-ledger/deployment.md` for full backup/restore procedures.
|
||||
|
||||
Quick reference:
|
||||
```bash
|
||||
# Backup
|
||||
pg_dump -Fc --dbname="$LEDGER_DB" --file ledger-$(date -u +%Y%m%d).dump
|
||||
|
||||
# Restore
|
||||
pg_restore -C -d postgres ledger-YYYYMMDD.dump
|
||||
|
||||
# Replay projections
|
||||
dotnet run --project tools/LedgerReplayHarness -- \
|
||||
--connection "$LEDGER_DB" --tenant all
|
||||
```
|
||||
|
||||
## Observability
|
||||
|
||||
Import the provided dashboards into your local Grafana instance:
|
||||
|
||||
```bash
|
||||
# Import via Grafana API or UI
|
||||
curl -X POST http://grafana:3000/api/dashboards/db \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @dashboards/findings-ledger.json
|
||||
```
|
||||
|
||||
Apply alert rules to Prometheus:
|
||||
```bash
|
||||
cp alerts/findings-ledger-alerts.yaml /etc/prometheus/rules.d/
|
||||
# Reload Prometheus
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Resolution |
|
||||
| --- | --- |
|
||||
| Migration fails | Check DB connectivity; verify user has CREATE/ALTER privileges |
|
||||
| Health check fails | Check logs: `docker logs findings-ledger` or `kubectl logs -l app.kubernetes.io/name=findings-ledger` |
|
||||
| Metrics not visible | Verify OTLP endpoint is reachable or use Prometheus scrape |
|
||||
| Staleness warnings | Import fresh advisory/VEX bundles via Mirror |
|
||||
|
||||
## Support
|
||||
|
||||
- Platform docs: `docs/modules/findings-ledger/`
|
||||
- Offline operation: `docs/24_OFFLINE_KIT.md`
|
||||
- Air-gap mode: `docs/airgap/`
|
||||
@@ -0,0 +1,122 @@
|
||||
# Findings Ledger Prometheus Alert Rules
|
||||
# Apply to Prometheus: cp findings-ledger-alerts.yaml /etc/prometheus/rules.d/
|
||||
|
||||
groups:
|
||||
- name: findings-ledger
|
||||
rules:
|
||||
# Service availability
|
||||
- alert: FindingsLedgerDown
|
||||
expr: up{job="findings-ledger"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger service is down"
|
||||
description: "Findings Ledger service has been unreachable for more than 2 minutes."
|
||||
|
||||
# Write latency
|
||||
- alert: FindingsLedgerHighWriteLatency
|
||||
expr: histogram_quantile(0.95, sum(rate(ledger_write_latency_seconds_bucket{job="findings-ledger"}[5m])) by (le)) > 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger write latency is high"
|
||||
description: "95th percentile write latency exceeds 1 second for 5 minutes. Current: {{ $value | humanizeDuration }}"
|
||||
|
||||
- alert: FindingsLedgerCriticalWriteLatency
|
||||
expr: histogram_quantile(0.95, sum(rate(ledger_write_latency_seconds_bucket{job="findings-ledger"}[5m])) by (le)) > 5
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger write latency is critically high"
|
||||
description: "95th percentile write latency exceeds 5 seconds. Current: {{ $value | humanizeDuration }}"
|
||||
|
||||
# Projection lag
|
||||
- alert: FindingsLedgerProjectionLag
|
||||
expr: ledger_projection_lag_seconds{job="findings-ledger"} > 30
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger projection lag is high"
|
||||
description: "Projection lag exceeds 30 seconds for 5 minutes. Current: {{ $value | humanizeDuration }}"
|
||||
|
||||
- alert: FindingsLedgerCriticalProjectionLag
|
||||
expr: ledger_projection_lag_seconds{job="findings-ledger"} > 300
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger projection lag is critically high"
|
||||
description: "Projection lag exceeds 5 minutes. Current: {{ $value | humanizeDuration }}"
|
||||
|
||||
# Merkle anchoring
|
||||
- alert: FindingsLedgerMerkleAnchorStale
|
||||
expr: time() - ledger_merkle_last_anchor_timestamp_seconds{job="findings-ledger"} > 600
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger Merkle anchor is stale"
|
||||
description: "No Merkle anchor created in the last 10 minutes. Last anchor: {{ $value | humanizeTimestamp }}"
|
||||
|
||||
- alert: FindingsLedgerMerkleAnchorFailed
|
||||
expr: increase(ledger_merkle_anchor_failures_total{job="findings-ledger"}[15m]) > 0
|
||||
for: 0m
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger Merkle anchoring failed"
|
||||
description: "Merkle anchor operation failed. Check logs for details."
|
||||
|
||||
# Database connectivity
|
||||
- alert: FindingsLedgerDatabaseErrors
|
||||
expr: increase(ledger_database_errors_total{job="findings-ledger"}[5m]) > 5
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger database errors detected"
|
||||
description: "More than 5 database errors in the last 5 minutes."
|
||||
|
||||
# Attachment storage
|
||||
- alert: FindingsLedgerAttachmentStorageErrors
|
||||
expr: increase(ledger_attachment_storage_errors_total{job="findings-ledger"}[15m]) > 0
|
||||
for: 0m
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Findings Ledger attachment storage errors"
|
||||
description: "Attachment storage operation failed. Check encryption keys and storage connectivity."
|
||||
|
||||
# Air-gap staleness (for offline environments)
|
||||
- alert: FindingsLedgerAdvisoryStaleness
|
||||
expr: ledger_airgap_advisory_staleness_seconds{job="findings-ledger"} > 604800
|
||||
for: 1h
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "Advisory data is stale in air-gapped environment"
|
||||
description: "Advisory data is older than 7 days. Import fresh data from Mirror."
|
||||
|
||||
- alert: FindingsLedgerVexStaleness
|
||||
expr: ledger_airgap_vex_staleness_seconds{job="findings-ledger"} > 604800
|
||||
for: 1h
|
||||
labels:
|
||||
severity: warning
|
||||
service: findings-ledger
|
||||
annotations:
|
||||
summary: "VEX data is stale in air-gapped environment"
|
||||
description: "VEX data is older than 7 days. Import fresh data from Mirror."
|
||||
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus"
|
||||
}
|
||||
],
|
||||
"__requires": [
|
||||
{
|
||||
"type": "grafana",
|
||||
"id": "grafana",
|
||||
"name": "Grafana",
|
||||
"version": "9.0.0"
|
||||
},
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "Findings Ledger service metrics and health",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"panels": [],
|
||||
"title": "Health Overview",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [
|
||||
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
|
||||
],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
|
||||
"id": 2,
|
||||
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [{ "expr": "up{job=\"findings-ledger\"}", "refId": "A" }],
|
||||
"title": "Service Status",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "palette-classic" }, "unit": "short" },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 },
|
||||
"id": 3,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [{ "expr": "ledger_events_total{job=\"findings-ledger\"}", "refId": "A" }],
|
||||
"title": "Total Events",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "thresholds" }, "unit": "s", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }, { "color": "red", "value": 5 }] } },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 },
|
||||
"id": 4,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [{ "expr": "ledger_projection_lag_seconds{job=\"findings-ledger\"}", "refId": "A" }],
|
||||
"title": "Projection Lag",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
|
||||
"id": 10,
|
||||
"panels": [],
|
||||
"title": "Write Performance",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "unit": "s" },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
|
||||
"id": 11,
|
||||
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [
|
||||
{ "expr": "histogram_quantile(0.50, sum(rate(ledger_write_latency_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p50", "refId": "A" },
|
||||
{ "expr": "histogram_quantile(0.95, sum(rate(ledger_write_latency_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p95", "refId": "B" },
|
||||
{ "expr": "histogram_quantile(0.99, sum(rate(ledger_write_latency_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p99", "refId": "C" }
|
||||
],
|
||||
"title": "Write Latency",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "unit": "ops" },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
|
||||
"id": 12,
|
||||
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [{ "expr": "rate(ledger_events_total{job=\"findings-ledger\"}[5m])", "legendFormat": "events/s", "refId": "A" }],
|
||||
"title": "Event Write Rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
|
||||
"id": 20,
|
||||
"panels": [],
|
||||
"title": "Merkle Anchoring",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "unit": "s" },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
|
||||
"id": 21,
|
||||
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [
|
||||
{ "expr": "histogram_quantile(0.50, sum(rate(ledger_merkle_anchor_duration_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p50", "refId": "A" },
|
||||
{ "expr": "histogram_quantile(0.95, sum(rate(ledger_merkle_anchor_duration_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p95", "refId": "B" }
|
||||
],
|
||||
"title": "Anchor Duration",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "thresholds" }, "unit": "short", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
|
||||
"id": 22,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
|
||||
"pluginVersion": "9.0.0",
|
||||
"targets": [{ "expr": "ledger_merkle_anchors_total{job=\"findings-ledger\"}", "refId": "A" }],
|
||||
"title": "Total Anchors",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": ["stellaops", "findings-ledger"],
|
||||
"templating": { "list": [] },
|
||||
"time": { "from": "now-1h", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "utc",
|
||||
"title": "Findings Ledger",
|
||||
"uid": "findings-ledger",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Container image tarballs populated at build time by offline-kit builder
|
||||
106
devops/services/findings-ledger/offline-kit/manifest.yaml
Normal file
106
devops/services/findings-ledger/offline-kit/manifest.yaml
Normal file
@@ -0,0 +1,106 @@
|
||||
# Findings Ledger Offline Kit Manifest
|
||||
# Version: 2025.11.0
|
||||
# Generated: 2025-12-07
|
||||
|
||||
apiVersion: stellaops.io/v1
|
||||
kind: OfflineKitManifest
|
||||
metadata:
|
||||
name: findings-ledger
|
||||
version: "2025.11.0"
|
||||
description: Findings Ledger service for event-sourced findings storage with Merkle anchoring
|
||||
|
||||
spec:
|
||||
components:
|
||||
- name: findings-ledger
|
||||
type: service
|
||||
image: stellaops/findings-ledger:2025.11.0
|
||||
digest: "" # Populated at build time
|
||||
|
||||
- name: findings-ledger-migrations
|
||||
type: job
|
||||
image: stellaops/findings-ledger-migrations:2025.11.0
|
||||
digest: "" # Populated at build time
|
||||
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: ">=14.0"
|
||||
type: database
|
||||
required: true
|
||||
|
||||
- name: otel-collector
|
||||
version: ">=0.80.0"
|
||||
type: service
|
||||
required: false
|
||||
description: Optional for telemetry export
|
||||
|
||||
migrations:
|
||||
- version: "001"
|
||||
file: migrations/001_initial_schema.sql
|
||||
checksum: "" # Populated at build time
|
||||
- version: "002"
|
||||
file: migrations/002_merkle_tables.sql
|
||||
checksum: ""
|
||||
- version: "003"
|
||||
file: migrations/003_attachments.sql
|
||||
checksum: ""
|
||||
- version: "004"
|
||||
file: migrations/004_projections.sql
|
||||
checksum: ""
|
||||
- version: "005"
|
||||
file: migrations/005_airgap_imports.sql
|
||||
checksum: ""
|
||||
- version: "006"
|
||||
file: migrations/006_evidence_snapshots.sql
|
||||
checksum: ""
|
||||
- version: "007"
|
||||
file: migrations/007_timeline_events.sql
|
||||
checksum: ""
|
||||
- version: "008"
|
||||
file: migrations/008_attestation_pointers.sql
|
||||
checksum: ""
|
||||
|
||||
dashboards:
|
||||
- name: findings-ledger
|
||||
file: dashboards/findings-ledger.json
|
||||
checksum: ""
|
||||
|
||||
alerts:
|
||||
- name: findings-ledger-alerts
|
||||
file: alerts/findings-ledger-alerts.yaml
|
||||
checksum: ""
|
||||
|
||||
configuration:
|
||||
required:
|
||||
- key: LEDGER__DB__CONNECTIONSTRING
|
||||
description: PostgreSQL connection string
|
||||
secret: true
|
||||
- key: LEDGER__ATTACHMENTS__ENCRYPTIONKEY
|
||||
description: AES-256 encryption key for attachments (base64)
|
||||
secret: true
|
||||
|
||||
optional:
|
||||
- key: LEDGER__MERKLE__SIGNINGKEY
|
||||
description: Signing key for Merkle root attestations
|
||||
secret: true
|
||||
- key: LEDGER__OBSERVABILITY__OTLPENDPOINT
|
||||
description: OpenTelemetry collector endpoint
|
||||
default: http://otel-collector:4317
|
||||
- key: LEDGER__MERKLE__ANCHORINTERVAL
|
||||
description: Merkle anchor interval (TimeSpan)
|
||||
default: "00:05:00"
|
||||
- key: LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD
|
||||
description: Advisory staleness threshold in seconds
|
||||
default: "604800"
|
||||
|
||||
verification:
|
||||
healthEndpoint: /health/ready
|
||||
metricsEndpoint: /metrics
|
||||
expectedMetrics:
|
||||
- ledger_write_latency_seconds
|
||||
- ledger_projection_lag_seconds
|
||||
- ledger_merkle_anchor_duration_seconds
|
||||
- ledger_events_total
|
||||
|
||||
checksums:
|
||||
algorithm: sha256
|
||||
manifest: "" # Populated at build time
|
||||
@@ -0,0 +1 @@
|
||||
# Database migration SQL scripts copied from StellaOps.FindingsLedger.Migrations
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# Import Findings Ledger container images into local Docker/containerd
|
||||
# Usage: ./import-images.sh [registry-prefix]
|
||||
#
|
||||
# Example:
|
||||
# ./import-images.sh # Loads as stellaops/*
|
||||
# ./import-images.sh myregistry.local/ # Loads and tags as myregistry.local/stellaops/*
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
IMAGES_DIR="${SCRIPT_DIR}/../images"
|
||||
REGISTRY_PREFIX="${1:-}"
|
||||
|
||||
# Color output helpers
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# Detect container runtime
|
||||
detect_runtime() {
|
||||
if command -v docker &>/dev/null; then
|
||||
echo "docker"
|
||||
elif command -v nerdctl &>/dev/null; then
|
||||
echo "nerdctl"
|
||||
elif command -v podman &>/dev/null; then
|
||||
echo "podman"
|
||||
else
|
||||
log_error "No container runtime found (docker, nerdctl, podman)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
RUNTIME=$(detect_runtime)
|
||||
log_info "Using container runtime: $RUNTIME"
|
||||
|
||||
# Load images from tarballs
|
||||
load_images() {
|
||||
local count=0
|
||||
|
||||
for tarball in "${IMAGES_DIR}"/*.tar; do
|
||||
if [[ -f "$tarball" ]]; then
|
||||
log_info "Loading image from: $(basename "$tarball")"
|
||||
|
||||
if $RUNTIME load -i "$tarball"; then
|
||||
((count++))
|
||||
else
|
||||
log_error "Failed to load: $tarball"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $count -eq 0 ]]; then
|
||||
log_warn "No image tarballs found in $IMAGES_DIR"
|
||||
log_warn "Run the offline kit builder first to populate images"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Loaded $count image(s)"
|
||||
}
|
||||
|
||||
# Re-tag images with custom registry prefix
|
||||
retag_images() {
|
||||
if [[ -z "$REGISTRY_PREFIX" ]]; then
|
||||
log_info "No registry prefix specified, skipping re-tag"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local images=(
|
||||
"stellaops/findings-ledger"
|
||||
"stellaops/findings-ledger-migrations"
|
||||
)
|
||||
|
||||
for image in "${images[@]}"; do
|
||||
# Get the loaded tag
|
||||
local loaded_tag
|
||||
loaded_tag=$($RUNTIME images --format '{{.Repository}}:{{.Tag}}' | grep "^${image}:" | head -1)
|
||||
|
||||
if [[ -n "$loaded_tag" ]]; then
|
||||
local new_tag="${REGISTRY_PREFIX}${loaded_tag}"
|
||||
log_info "Re-tagging: $loaded_tag -> $new_tag"
|
||||
$RUNTIME tag "$loaded_tag" "$new_tag"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Verify loaded images
|
||||
verify_images() {
|
||||
log_info "Verifying loaded images..."
|
||||
|
||||
local images=(
|
||||
"stellaops/findings-ledger"
|
||||
"stellaops/findings-ledger-migrations"
|
||||
)
|
||||
|
||||
local missing=0
|
||||
for image in "${images[@]}"; do
|
||||
if $RUNTIME images --format '{{.Repository}}' | grep -q "^${REGISTRY_PREFIX}${image}$"; then
|
||||
log_info " ✓ ${REGISTRY_PREFIX}${image}"
|
||||
else
|
||||
log_error " ✗ ${REGISTRY_PREFIX}${image} not found"
|
||||
((missing++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $missing -gt 0 ]]; then
|
||||
log_error "$missing image(s) missing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "All images verified"
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "Findings Ledger - Image Import"
|
||||
log_info "=============================="
|
||||
|
||||
load_images
|
||||
retag_images
|
||||
verify_images
|
||||
|
||||
log_info "Image import complete"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run Findings Ledger database migrations
|
||||
# Usage: ./run-migrations.sh [connection-string]
|
||||
#
|
||||
# Environment variables:
|
||||
# LEDGER__DB__CONNECTIONSTRING - PostgreSQL connection string (if not provided as arg)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MIGRATIONS_DIR="${SCRIPT_DIR}/../migrations"
|
||||
|
||||
# Color output helpers
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# Get connection string
|
||||
CONNECTION_STRING="${1:-${LEDGER__DB__CONNECTIONSTRING:-}}"
|
||||
|
||||
if [[ -z "$CONNECTION_STRING" ]]; then
|
||||
log_error "Connection string required"
|
||||
echo "Usage: $0 <connection-string>"
|
||||
echo " or set LEDGER__DB__CONNECTIONSTRING environment variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect container runtime
|
||||
detect_runtime() {
|
||||
if command -v docker &>/dev/null; then
|
||||
echo "docker"
|
||||
elif command -v nerdctl &>/dev/null; then
|
||||
echo "nerdctl"
|
||||
elif command -v podman &>/dev/null; then
|
||||
echo "podman"
|
||||
else
|
||||
log_error "No container runtime found"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
RUNTIME=$(detect_runtime)
|
||||
|
||||
# Run migrations via container
|
||||
run_migrations_container() {
|
||||
log_info "Running migrations via container..."
|
||||
|
||||
$RUNTIME run --rm \
|
||||
-e "LEDGER__DB__CONNECTIONSTRING=${CONNECTION_STRING}" \
|
||||
--network host \
|
||||
stellaops/findings-ledger-migrations:2025.11.0 \
|
||||
--connection "$CONNECTION_STRING"
|
||||
}
|
||||
|
||||
# Alternative: Run migrations via psql (if dotnet not available)
|
||||
run_migrations_psql() {
|
||||
log_info "Running migrations via psql..."
|
||||
|
||||
if ! command -v psql &>/dev/null; then
|
||||
log_error "psql not found and container runtime unavailable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse connection string for psql
|
||||
# Expected format: Host=...;Port=...;Database=...;Username=...;Password=...
|
||||
local host port database username password
|
||||
host=$(echo "$CONNECTION_STRING" | grep -oP 'Host=\K[^;]+')
|
||||
port=$(echo "$CONNECTION_STRING" | grep -oP 'Port=\K[^;]+' || echo "5432")
|
||||
database=$(echo "$CONNECTION_STRING" | grep -oP 'Database=\K[^;]+')
|
||||
username=$(echo "$CONNECTION_STRING" | grep -oP 'Username=\K[^;]+')
|
||||
password=$(echo "$CONNECTION_STRING" | grep -oP 'Password=\K[^;]+')
|
||||
|
||||
export PGPASSWORD="$password"
|
||||
|
||||
for migration in "${MIGRATIONS_DIR}"/*.sql; do
|
||||
if [[ -f "$migration" ]]; then
|
||||
log_info "Applying: $(basename "$migration")"
|
||||
psql -h "$host" -p "$port" -U "$username" -d "$database" -f "$migration"
|
||||
fi
|
||||
done
|
||||
|
||||
unset PGPASSWORD
|
||||
}
|
||||
|
||||
verify_connection() {
|
||||
log_info "Verifying database connection..."
|
||||
|
||||
# Try container-based verification
|
||||
if $RUNTIME run --rm \
|
||||
--network host \
|
||||
postgres:14-alpine \
|
||||
pg_isready -h "$(echo "$CONNECTION_STRING" | grep -oP 'Host=\K[^;]+')" \
|
||||
-p "$(echo "$CONNECTION_STRING" | grep -oP 'Port=\K[^;]+' || echo 5432)" \
|
||||
&>/dev/null; then
|
||||
log_info "Database connection verified"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Could not verify database connection (may still work)"
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "Findings Ledger - Database Migrations"
|
||||
log_info "======================================"
|
||||
|
||||
verify_connection
|
||||
|
||||
# Prefer container-based migrations
|
||||
if $RUNTIME image inspect stellaops/findings-ledger-migrations:2025.11.0 &>/dev/null; then
|
||||
run_migrations_container
|
||||
else
|
||||
log_warn "Migration image not found, falling back to psql"
|
||||
run_migrations_psql
|
||||
fi
|
||||
|
||||
log_info "Migrations complete"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify Findings Ledger installation
|
||||
# Usage: ./verify-install.sh [service-url]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE_URL="${1:-http://localhost:8188}"
|
||||
|
||||
# Color output helpers
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
log_pass() { echo -e "${GREEN} ✓${NC} $*"; }
|
||||
log_fail() { echo -e "${RED} ✗${NC} $*"; }
|
||||
|
||||
CHECKS_PASSED=0
|
||||
CHECKS_FAILED=0
|
||||
|
||||
run_check() {
|
||||
local name="$1"
|
||||
local cmd="$2"
|
||||
|
||||
if eval "$cmd" &>/dev/null; then
|
||||
log_pass "$name"
|
||||
((CHECKS_PASSED++))
|
||||
else
|
||||
log_fail "$name"
|
||||
((CHECKS_FAILED++))
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "Findings Ledger - Installation Verification"
|
||||
log_info "==========================================="
|
||||
log_info "Service URL: $SERVICE_URL"
|
||||
echo ""
|
||||
|
||||
log_info "Health Checks:"
|
||||
run_check "Readiness endpoint" "curl -sf ${SERVICE_URL}/health/ready"
|
||||
run_check "Liveness endpoint" "curl -sf ${SERVICE_URL}/health/live"
|
||||
|
||||
echo ""
|
||||
log_info "Metrics Checks:"
|
||||
run_check "Metrics endpoint available" "curl -sf ${SERVICE_URL}/metrics | head -1"
|
||||
run_check "ledger_write_latency_seconds present" "curl -sf ${SERVICE_URL}/metrics | grep -q ledger_write_latency_seconds"
|
||||
run_check "ledger_projection_lag_seconds present" "curl -sf ${SERVICE_URL}/metrics | grep -q ledger_projection_lag_seconds"
|
||||
run_check "ledger_merkle_anchor_duration_seconds present" "curl -sf ${SERVICE_URL}/metrics | grep -q ledger_merkle_anchor_duration_seconds"
|
||||
|
||||
echo ""
|
||||
log_info "API Checks:"
|
||||
run_check "OpenAPI spec available" "curl -sf ${SERVICE_URL}/swagger/v1/swagger.json | head -1"
|
||||
|
||||
echo ""
|
||||
log_info "========================================"
|
||||
log_info "Results: ${CHECKS_PASSED} passed, ${CHECKS_FAILED} failed"
|
||||
|
||||
if [[ $CHECKS_FAILED -gt 0 ]]; then
|
||||
log_error "Some checks failed. Review service logs for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "All checks passed. Installation verified."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
42
devops/services/graph-indexer/release-plan.md
Normal file
42
devops/services/graph-indexer/release-plan.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Graph Indexer Release/Offline Bundle Plan (DEVOPS-GRAPH-INDEX-28-010-REL)
|
||||
|
||||
## Goals
|
||||
- Publish signed Helm/Compose bundles for Graph Indexer with offline parity.
|
||||
- Provide SBOM + attestations for images/charts and reproducible artefacts for air-gap kits.
|
||||
|
||||
## Artefacts
|
||||
- Helm chart + values overrides (offline/airgap).
|
||||
- Docker/OCI images (indexer, api) pinned by digest.
|
||||
- SBOMs (SPDX JSON) for images and chart.
|
||||
- Cosign attestations for images and chart tarball.
|
||||
- Offline bundle: tarball containing images (oras layout), charts, values, SBOMs, attestations, and `SHA256SUMS`.
|
||||
|
||||
## Pipeline outline
|
||||
1) **Build** images (indexer + api) with SBOM generation (`syft`), tag and record digests.
|
||||
2) **Sign** images with cosign key (KMS for online; file key for offline bundle) and produce attestations.
|
||||
3) **Chart package**: render chart, package to `.tgz`, generate SBOM for chart, sign with cosign.
|
||||
4) **Compose export**: render Compose file with pinned digests and non-root users.
|
||||
5) **Bundle**: assemble offline tarball:
|
||||
- `images/` oras layout with signed images
|
||||
- `charts/graph-indexer.tgz` + signature
|
||||
- `compose/graph-indexer.yml` (pinned digests)
|
||||
- `sboms/` for images + chart
|
||||
- `attestations/` (cosign bundles)
|
||||
- `SHA256SUMS` and `SHA256SUMS.sig`
|
||||
6) **Verify step**: pipeline stage runs `cosign verify`, `sha256sum --check`, and `helm template` smoke render with airgap values.
|
||||
7) **Publish**: upload to artefact store + offline kit; write manifest with hashes/versions.
|
||||
|
||||
## Security/hardening
|
||||
- Non-root images, read-only rootfs, drop NET_RAW, seccomp default.
|
||||
- Telemetry disabled; no registry pulls at runtime.
|
||||
- mTLS between indexer and dependencies (documented values).
|
||||
|
||||
## Evidence to capture
|
||||
- Image digests, SBOM hashes, cosign verification logs.
|
||||
- Bundle `SHA256SUMS` and signed manifest.
|
||||
- Helm/Compose render outputs (short).
|
||||
|
||||
## Owners
|
||||
- DevOps Guild (build/pipeline)
|
||||
- Graph Indexer Guild (chart/values)
|
||||
- Platform Security (signing policy)
|
||||
128
devops/services/ledger/build-pack.sh
Normal file
128
devops/services/ledger/build-pack.sh
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build Findings Ledger export pack
|
||||
# Usage: ./build-pack.sh [--snapshot-id <id>] [--sign] [--output <dir>]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
|
||||
OUT_DIR="${OUT_DIR:-$ROOT/out/ledger/packs}"
|
||||
SNAPSHOT_ID="${SNAPSHOT_ID:-$(date +%Y%m%d%H%M%S)}"
|
||||
CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
SIGN=0
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--snapshot-id) SNAPSHOT_ID="$2"; shift 2 ;;
|
||||
--output) OUT_DIR="$2"; shift 2 ;;
|
||||
--sign) SIGN=1; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$OUT_DIR/staging"
|
||||
|
||||
echo "==> Building Ledger Pack"
|
||||
echo " Snapshot ID: $SNAPSHOT_ID"
|
||||
echo " Output: $OUT_DIR"
|
||||
|
||||
# Key resolution for signing
|
||||
resolve_key() {
|
||||
if [[ -n "${COSIGN_PRIVATE_KEY_B64:-}" ]]; then
|
||||
local tmp_key="$OUT_DIR/.cosign.key"
|
||||
echo "$COSIGN_PRIVATE_KEY_B64" | base64 -d > "$tmp_key"
|
||||
chmod 600 "$tmp_key"
|
||||
echo "$tmp_key"
|
||||
elif [[ -f "$ROOT/tools/cosign/cosign.key" ]]; then
|
||||
echo "$ROOT/tools/cosign/cosign.key"
|
||||
elif [[ "${COSIGN_ALLOW_DEV_KEY:-0}" == "1" && -f "$ROOT/tools/cosign/cosign.dev.key" ]]; then
|
||||
echo "[info] Using development key" >&2
|
||||
echo "$ROOT/tools/cosign/cosign.dev.key"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Create pack structure
|
||||
STAGE="$OUT_DIR/staging/$SNAPSHOT_ID"
|
||||
mkdir -p "$STAGE/findings" "$STAGE/metadata" "$STAGE/signatures"
|
||||
|
||||
# Create placeholder data (replace with actual Ledger export)
|
||||
cat > "$STAGE/findings/findings.ndjson" <<EOF
|
||||
{"id": "placeholder-1", "type": "infrastructure-ready", "created": "$CREATED"}
|
||||
EOF
|
||||
|
||||
cat > "$STAGE/metadata/snapshot.json" <<EOF
|
||||
{
|
||||
"snapshotId": "$SNAPSHOT_ID",
|
||||
"created": "$CREATED",
|
||||
"format": "ledger-pack-v1",
|
||||
"status": "infrastructure-ready",
|
||||
"note": "Replace with actual Ledger snapshot export"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Generate manifest
|
||||
sha256() { sha256sum "$1" | awk '{print $1}'; }
|
||||
|
||||
cat > "$STAGE/manifest.json" <<EOF
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"packId": "$SNAPSHOT_ID",
|
||||
"created": "$CREATED",
|
||||
"format": "ledger-pack-v1",
|
||||
"contents": {
|
||||
"findings": {"path": "findings/findings.ndjson", "format": "ndjson"},
|
||||
"metadata": {"path": "metadata/snapshot.json", "format": "json"}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Generate provenance
|
||||
cat > "$STAGE/provenance.json" <<EOF
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [{"name": "snapshot-$SNAPSHOT_ID.pack.tar.gz", "digest": {"sha256": "pending"}}],
|
||||
"predicateType": "https://slsa.dev/provenance/v1",
|
||||
"predicate": {
|
||||
"buildDefinition": {
|
||||
"buildType": "https://stella-ops.org/ledger-pack/v1",
|
||||
"internalParameters": {"snapshotId": "$SNAPSHOT_ID", "created": "$CREATED"}
|
||||
},
|
||||
"runDetails": {"builder": {"id": "https://stella-ops.org/ledger-pack-builder"}}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create pack tarball
|
||||
PACK_TAR="$OUT_DIR/snapshot-$SNAPSHOT_ID.pack.tar.gz"
|
||||
tar -czf "$PACK_TAR" -C "$STAGE" .
|
||||
|
||||
# Update provenance with actual hash
|
||||
PACK_HASH=$(sha256 "$PACK_TAR")
|
||||
sed -i "s/\"sha256\": \"pending\"/\"sha256\": \"$PACK_HASH\"/" "$STAGE/provenance.json" 2>/dev/null || \
|
||||
sed -i '' "s/\"sha256\": \"pending\"/\"sha256\": \"$PACK_HASH\"/" "$STAGE/provenance.json"
|
||||
|
||||
# Generate checksums
|
||||
cd "$OUT_DIR"
|
||||
sha256sum "snapshot-$SNAPSHOT_ID.pack.tar.gz" > "snapshot-$SNAPSHOT_ID.SHA256SUMS"
|
||||
|
||||
# Sign if requested
|
||||
if [[ $SIGN -eq 1 ]]; then
|
||||
KEY_FILE=$(resolve_key)
|
||||
if [[ -n "$KEY_FILE" ]] && command -v cosign &>/dev/null; then
|
||||
echo "==> Signing pack..."
|
||||
COSIGN_PASSWORD="${COSIGN_PASSWORD:-}" cosign sign-blob \
|
||||
--key "$KEY_FILE" \
|
||||
--bundle "$OUT_DIR/snapshot-$SNAPSHOT_ID.dsse.json" \
|
||||
--tlog-upload=false --yes "$PACK_TAR" 2>/dev/null || echo "[info] Signing skipped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$OUT_DIR/staging"
|
||||
[[ -f "$OUT_DIR/.cosign.key" ]] && rm -f "$OUT_DIR/.cosign.key"
|
||||
|
||||
echo "==> Pack build complete"
|
||||
echo " Pack: $PACK_TAR"
|
||||
echo " Checksums: $OUT_DIR/snapshot-$SNAPSHOT_ID.SHA256SUMS"
|
||||
61
devops/services/ledger/deprecation-policy.yaml
Normal file
61
devops/services/ledger/deprecation-policy.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
# Findings Ledger API Deprecation Policy
|
||||
# DEVOPS-LEDGER-OAS-63-001-REL
|
||||
|
||||
version: "1.0.0"
|
||||
created: "2025-12-14"
|
||||
|
||||
policy:
|
||||
# Minimum deprecation notice period
|
||||
notice_period_days: 90
|
||||
|
||||
# Supported API versions
|
||||
supported_versions:
|
||||
- version: "v1"
|
||||
status: "current"
|
||||
sunset_date: null
|
||||
# Future versions will be added here
|
||||
|
||||
# Deprecation workflow
|
||||
workflow:
|
||||
- stage: "announce"
|
||||
description: "Add deprecation notice to API responses and docs"
|
||||
actions:
|
||||
- "Add Sunset header to deprecated endpoints"
|
||||
- "Update OpenAPI spec with deprecation annotations"
|
||||
- "Notify consumers via changelog"
|
||||
|
||||
- stage: "warn"
|
||||
description: "Emit warnings in logs and metrics"
|
||||
duration_days: 30
|
||||
actions:
|
||||
- "Log deprecation warnings"
|
||||
- "Increment deprecation_usage_total metric"
|
||||
- "Send email to registered consumers"
|
||||
|
||||
- stage: "sunset"
|
||||
description: "Remove deprecated endpoints"
|
||||
actions:
|
||||
- "Return 410 Gone for removed endpoints"
|
||||
- "Update SDK to remove deprecated methods"
|
||||
- "Archive endpoint documentation"
|
||||
|
||||
# HTTP headers for deprecation
|
||||
headers:
|
||||
sunset: "Sunset"
|
||||
deprecation: "Deprecation"
|
||||
link: "Link"
|
||||
|
||||
# Metrics to track
|
||||
metrics:
|
||||
- name: "ledger_api_deprecation_usage_total"
|
||||
type: "counter"
|
||||
labels: ["endpoint", "version", "consumer"]
|
||||
description: "Usage count of deprecated endpoints"
|
||||
|
||||
- name: "ledger_api_version_requests_total"
|
||||
type: "counter"
|
||||
labels: ["version"]
|
||||
description: "Requests per API version"
|
||||
|
||||
# Current deprecations (none yet)
|
||||
deprecations: []
|
||||
56
devops/services/ledger/oas-infrastructure.md
Normal file
56
devops/services/ledger/oas-infrastructure.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Findings Ledger OpenAPI Infrastructure
|
||||
|
||||
## Scope
|
||||
Infrastructure for Ledger OAS lint, publish, SDK generation, and deprecation governance.
|
||||
|
||||
## Tasks Covered
|
||||
- DEVOPS-LEDGER-OAS-61-001-REL: Lint/diff/publish gates
|
||||
- DEVOPS-LEDGER-OAS-61-002-REL: `.well-known/openapi` validation
|
||||
- DEVOPS-LEDGER-OAS-62-001-REL: SDK generation/signing
|
||||
- DEVOPS-LEDGER-OAS-63-001-REL: Deprecation governance
|
||||
|
||||
## File Structure
|
||||
```
|
||||
ops/devops/ledger/
|
||||
├── oas-infrastructure.md (this file)
|
||||
├── validate-oas.sh # Lint + validate OAS spec
|
||||
├── generate-sdk.sh # Generate and sign SDK
|
||||
├── publish-oas.sh # Publish to .well-known
|
||||
└── deprecation-policy.yaml # Deprecation rules
|
||||
|
||||
.gitea/workflows/
|
||||
├── ledger-oas-ci.yml # OAS lint/validate/diff
|
||||
├── ledger-sdk-release.yml # SDK generation
|
||||
└── ledger-oas-publish.yml # Publish spec
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
- Findings Ledger OpenAPI spec at `api/ledger/openapi.yaml`
|
||||
- Version info in spec metadata
|
||||
- Examples for each endpoint
|
||||
|
||||
## Usage
|
||||
|
||||
### Validate OAS
|
||||
```bash
|
||||
./ops/devops/ledger/validate-oas.sh api/ledger/openapi.yaml
|
||||
```
|
||||
|
||||
### Generate SDK
|
||||
```bash
|
||||
# Dev mode
|
||||
COSIGN_ALLOW_DEV_KEY=1 ./ops/devops/ledger/generate-sdk.sh
|
||||
|
||||
# Production
|
||||
./ops/devops/ledger/generate-sdk.sh
|
||||
```
|
||||
|
||||
### Publish to .well-known
|
||||
```bash
|
||||
./ops/devops/ledger/publish-oas.sh --environment staging
|
||||
```
|
||||
|
||||
## Outputs
|
||||
- `out/ledger/sdk/` - Generated SDK packages
|
||||
- `out/ledger/oas/` - Validated spec + diff reports
|
||||
- `out/ledger/deprecation/` - Deprecation reports
|
||||
58
devops/services/ledger/packs-infrastructure.md
Normal file
58
devops/services/ledger/packs-infrastructure.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Findings Ledger Packs Infrastructure
|
||||
|
||||
## Scope
|
||||
Infrastructure for snapshot/time-travel export packaging and signing.
|
||||
|
||||
## Tasks Covered
|
||||
- DEVOPS-LEDGER-PACKS-42-001-REL: Snapshot/time-travel export packaging
|
||||
- DEVOPS-LEDGER-PACKS-42-002-REL: Pack signing + integrity verification
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Pack Builder
|
||||
Creates deterministic export packs from Ledger snapshots.
|
||||
|
||||
```bash
|
||||
# Build pack from snapshot
|
||||
./ops/devops/ledger/build-pack.sh --snapshot-id <id> --output out/ledger/packs/
|
||||
|
||||
# Dev mode with signing
|
||||
COSIGN_ALLOW_DEV_KEY=1 ./ops/devops/ledger/build-pack.sh --sign
|
||||
```
|
||||
|
||||
### 2. Pack Verifier
|
||||
Verifies pack integrity and signatures.
|
||||
|
||||
```bash
|
||||
# Verify pack
|
||||
./ops/devops/ledger/verify-pack.sh out/ledger/packs/snapshot-*.pack.tar.gz
|
||||
```
|
||||
|
||||
### 3. Time-Travel Export
|
||||
Creates point-in-time exports for compliance/audit.
|
||||
|
||||
```bash
|
||||
# Export at specific timestamp
|
||||
./ops/devops/ledger/time-travel-export.sh --timestamp 2025-12-01T00:00:00Z
|
||||
```
|
||||
|
||||
## Pack Format
|
||||
```
|
||||
snapshot-<id>.pack.tar.gz
|
||||
├── manifest.json # Pack metadata + checksums
|
||||
├── findings/ # Finding records (NDJSON)
|
||||
├── metadata/ # Scan metadata
|
||||
├── provenance.json # SLSA provenance
|
||||
└── signatures/
|
||||
├── manifest.dsse.json # DSSE signature
|
||||
└── SHA256SUMS # Checksums
|
||||
```
|
||||
|
||||
## CI Workflows
|
||||
- `ledger-packs-ci.yml` - Build and verify packs
|
||||
- `ledger-packs-release.yml` - Sign and publish packs
|
||||
|
||||
## Prerequisites
|
||||
- Ledger snapshot schema finalized
|
||||
- Storage contract defined
|
||||
- Pack format specification
|
||||
80
devops/services/ledger/validate-oas.sh
Normal file
80
devops/services/ledger/validate-oas.sh
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate Findings Ledger OpenAPI spec
|
||||
# Usage: ./validate-oas.sh [spec-path]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
|
||||
SPEC_PATH="${1:-$ROOT/api/ledger/openapi.yaml}"
|
||||
OUT_DIR="${OUT_DIR:-$ROOT/out/ledger/oas}"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
echo "==> Validating Ledger OpenAPI Spec"
|
||||
echo " Spec: $SPEC_PATH"
|
||||
|
||||
# Check if spec exists
|
||||
if [[ ! -f "$SPEC_PATH" ]]; then
|
||||
echo "[info] OpenAPI spec not found at $SPEC_PATH"
|
||||
echo "[info] Creating placeholder for infrastructure validation"
|
||||
|
||||
mkdir -p "$(dirname "$SPEC_PATH")"
|
||||
cat > "$SPEC_PATH" <<'EOF'
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Findings Ledger API
|
||||
version: 0.0.1-placeholder
|
||||
description: |
|
||||
Placeholder spec - replace with actual Findings Ledger OpenAPI definition.
|
||||
Infrastructure is ready for validation once spec is provided.
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: Health check
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
EOF
|
||||
echo "[info] Placeholder spec created"
|
||||
fi
|
||||
|
||||
# Lint with spectral if available
|
||||
if command -v spectral &>/dev/null; then
|
||||
echo "==> Running Spectral lint..."
|
||||
spectral lint "$SPEC_PATH" --output "$OUT_DIR/lint-report.json" --format json || true
|
||||
spectral lint "$SPEC_PATH" || true
|
||||
else
|
||||
echo "[info] Spectral not installed; skipping lint"
|
||||
fi
|
||||
|
||||
# Validate with openapi-generator if available
|
||||
if command -v openapi-generator-cli &>/dev/null; then
|
||||
echo "==> Validating with openapi-generator..."
|
||||
openapi-generator-cli validate -i "$SPEC_PATH" > "$OUT_DIR/validation-report.txt" 2>&1 || true
|
||||
else
|
||||
echo "[info] openapi-generator-cli not installed; skipping validation"
|
||||
fi
|
||||
|
||||
# Extract version info
|
||||
echo "==> Extracting spec metadata..."
|
||||
if command -v yq &>/dev/null; then
|
||||
VERSION=$(yq '.info.version' "$SPEC_PATH")
|
||||
TITLE=$(yq '.info.title' "$SPEC_PATH")
|
||||
else
|
||||
VERSION="unknown"
|
||||
TITLE="Findings Ledger API"
|
||||
fi
|
||||
|
||||
# Generate summary
|
||||
cat > "$OUT_DIR/spec-summary.json" <<EOF
|
||||
{
|
||||
"specPath": "$SPEC_PATH",
|
||||
"title": "$TITLE",
|
||||
"version": "$VERSION",
|
||||
"validatedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"status": "validated"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> Validation complete"
|
||||
echo " Summary: $OUT_DIR/spec-summary.json"
|
||||
46
devops/services/orchestrator-config/README.md
Normal file
46
devops/services/orchestrator-config/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Orchestrator Infra Bootstrap (DEVOPS-ORCH-32-001)
|
||||
|
||||
## Components
|
||||
- Postgres 16 (state/config)
|
||||
- Mongo 7 (job ledger history)
|
||||
- NATS 2.10 JetStream (queue/bus)
|
||||
|
||||
Compose file: `ops/devops/orchestrator/docker-compose.orchestrator.yml`
|
||||
|
||||
## Quick start (offline-friendly)
|
||||
```bash
|
||||
# bring up infra
|
||||
COMPOSE_FILE=ops/devops/orchestrator/docker-compose.orchestrator.yml docker compose up -d
|
||||
|
||||
# smoke check and emit connection strings
|
||||
scripts/orchestrator/smoke.sh
|
||||
cat out/orchestrator-smoke/readiness.txt
|
||||
|
||||
# synthetic probe (postgres/mongo/nats health)
|
||||
scripts/orchestrator/probe.sh
|
||||
cat out/orchestrator-probe/status.txt
|
||||
|
||||
# replay readiness (restart then smoke)
|
||||
scripts/orchestrator/replay-smoke.sh
|
||||
```
|
||||
|
||||
Connection strings
|
||||
- Postgres: `postgres://orch:orchpass@localhost:55432/orchestrator`
|
||||
- Mongo: `mongodb://localhost:57017`
|
||||
- NATS: `nats://localhost:4222`
|
||||
|
||||
## Observability
|
||||
- Alerts: `ops/devops/orchestrator/alerts.yaml`
|
||||
- Grafana dashboard: `ops/devops/orchestrator/grafana/orchestrator-overview.json`
|
||||
- Metrics expected: `job_queue_depth`, `job_failures_total`, `lease_extensions_total`, `job_latency_seconds_bucket`.
|
||||
- Runbook: `ops/devops/orchestrator/incident-response.md`
|
||||
- Synthetic probes: `scripts/orchestrator/probe.sh` (writes `out/orchestrator-probe/status.txt`).
|
||||
- Replay smoke: `scripts/orchestrator/replay-smoke.sh` (idempotent restart + smoke).
|
||||
|
||||
## CI hook (suggested)
|
||||
Add a workflow step (or local cron) to run `scripts/orchestrator/smoke.sh` with `SKIP_UP=1` against existing infra and publish the `readiness.txt` artifact for traceability.
|
||||
|
||||
## Notes
|
||||
- Uses fixed ports for determinism; adjust via COMPOSE overrides if needed.
|
||||
- Data volumes: `orch_pg_data`, `orch_mongo_data` (docker volumes).
|
||||
- No external downloads beyond base images; pin images to specific tags above.
|
||||
69
devops/services/orchestrator-config/alerts.yaml
Normal file
69
devops/services/orchestrator-config/alerts.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
groups:
|
||||
- name: orchestrator-core
|
||||
rules:
|
||||
- alert: OrchestratorQueueDepthHigh
|
||||
expr: job_queue_depth > 500
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "Queue depth high"
|
||||
description: "job_queue_depth exceeded 500 for 10m"
|
||||
- alert: OrchestratorFailuresHigh
|
||||
expr: rate(job_failures_total[5m]) > 5
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "Job failures elevated"
|
||||
description: "Failure rate above 5/min in last 5m"
|
||||
- alert: OrchestratorLeaseStall
|
||||
expr: rate(lease_extensions_total[5m]) == 0 and job_queue_depth > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "Leases stalled"
|
||||
description: "No lease renewals while queue has items"
|
||||
- alert: OrchestratorDLQDepthHigh
|
||||
expr: job_dlq_depth > 10
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "DLQ depth high"
|
||||
description: "Dead-letter queue depth above 10 for 10m"
|
||||
- alert: OrchestratorBackpressure
|
||||
expr: avg_over_time(rate_limiter_backpressure_ratio[5m]) > 0.5
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "Backpressure elevated"
|
||||
description: "Rate limiter backpressure >50% over 5m"
|
||||
- alert: OrchestratorErrorCluster
|
||||
expr: sum by(jobType) (rate(job_failures_total[5m])) > 3
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "Error cluster detected"
|
||||
description: "Failure rate >3/min for a job type"
|
||||
- alert: OrchestratorFailureBurnRateHigh
|
||||
expr: |
|
||||
(rate(job_failures_total[5m]) / clamp_min(rate(job_processed_total[5m]), 1)) > 0.02
|
||||
and
|
||||
(rate(job_failures_total[30m]) / clamp_min(rate(job_processed_total[30m]), 1)) > 0.01
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
service: orchestrator
|
||||
annotations:
|
||||
summary: "Failure burn rate breaching SLO"
|
||||
description: "5m/30m failure burn rate above 2%/1% SLO; investigate upstream jobs and dependencies."
|
||||
@@ -0,0 +1,49 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
orchestrator-postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: orch
|
||||
POSTGRES_PASSWORD: orchpass
|
||||
POSTGRES_DB: orchestrator
|
||||
volumes:
|
||||
- orch_pg_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "55432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U orch"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
orchestrator-mongo:
|
||||
image: mongo:7
|
||||
command: ["mongod", "--quiet", "--storageEngine=wiredTiger"]
|
||||
ports:
|
||||
- "57017:27017"
|
||||
volumes:
|
||||
- orch_mongo_data:/data/db
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
orchestrator-nats:
|
||||
image: nats:2.10-alpine
|
||||
ports:
|
||||
- "5422:4222"
|
||||
- "5822:8222"
|
||||
command: ["-js", "-m", "8222"]
|
||||
healthcheck:
|
||||
test: ["CMD", "nats", "--server", "localhost:4222", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
orch_pg_data:
|
||||
orch_mongo_data:
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"schemaVersion": 39,
|
||||
"title": "Orchestrator Overview",
|
||||
"panels": [
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Queue Depth",
|
||||
"datasource": "Prometheus",
|
||||
"fieldConfig": {"defaults": {"unit": "none"}},
|
||||
"targets": [{"expr": "sum(job_queue_depth)"}]
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Queue Depth by Job Type",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "job_queue_depth"}],
|
||||
"fieldConfig": {"defaults": {"unit": "none"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Failures per minute",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "rate(job_failures_total[5m])"}],
|
||||
"fieldConfig": {"defaults": {"unit": "short"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Leases per second",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "rate(lease_extensions_total[5m])"}],
|
||||
"fieldConfig": {"defaults": {"unit": "ops"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Job latency p95",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "histogram_quantile(0.95, sum(rate(job_latency_seconds_bucket[5m])) by (le))"}],
|
||||
"fieldConfig": {"defaults": {"unit": "s"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "DLQ depth",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "job_dlq_depth"}],
|
||||
"fieldConfig": {"defaults": {"unit": "none"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Backpressure ratio",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "rate_limiter_backpressure_ratio"}],
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Failures by job type",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [{"expr": "rate(job_failures_total[5m])"}],
|
||||
"fieldConfig": {"defaults": {"unit": "short"}}
|
||||
}
|
||||
],
|
||||
"time": {"from": "now-6h", "to": "now"}
|
||||
}
|
||||
37
devops/services/orchestrator-config/incident-response.md
Normal file
37
devops/services/orchestrator-config/incident-response.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Orchestrator Incident Response & GA Readiness
|
||||
|
||||
## Alert links
|
||||
- Prometheus rules: `ops/devops/orchestrator/alerts.yaml` (includes burn-rate).
|
||||
- Dashboard: `ops/devops/orchestrator/grafana/orchestrator-overview.json`.
|
||||
|
||||
## Runbook (by alert)
|
||||
- **QueueDepthHigh / DLQDepthHigh**
|
||||
- Check backlog cause: slow workers vs. downstream dependency.
|
||||
- Scale workers + clear DLQ after snapshot; if DLQ cause is transient, replay via `replay-smoke.sh` after fixes.
|
||||
- **FailuresHigh / ErrorCluster / FailureBurnRateHigh**
|
||||
- Inspect failing job type from alert labels.
|
||||
- Pause new dispatch for the job type; ship hotfix or rollback offending worker image.
|
||||
- Validate with `scripts/orchestrator/probe.sh` then `smoke.sh` to ensure infra is healthy.
|
||||
- **LeaseStall**
|
||||
- Look for stuck locks in Postgres `locks` view; force release or restart the worker set.
|
||||
- Confirm NATS health (probe) and worker heartbeats.
|
||||
- **Backpressure**
|
||||
- Increase rate-limit budgets temporarily; ensure backlog drains; restore defaults after stability.
|
||||
|
||||
## Synthetic checks
|
||||
- `scripts/orchestrator/probe.sh` — psql ping, mongo ping, NATS pub/ping; writes `out/orchestrator-probe/status.txt`.
|
||||
- `scripts/orchestrator/smoke.sh` — end-to-end infra smoke, emits readiness.
|
||||
- `scripts/orchestrator/replay-smoke.sh` — restart stack then run smoke to prove restart/replay works.
|
||||
|
||||
## GA readiness checklist
|
||||
- [ ] Burn-rate alerting enabled in Prometheus/Alertmanager (see `alerts.yaml` rule `OrchestratorFailureBurnRateHigh`).
|
||||
- [ ] Dashboard imported and linked in on-call rotation.
|
||||
- [ ] Synthetic probe cron in CI/ops runner publishing `status.txt` artifact daily.
|
||||
- [ ] Replay smoke scheduled post-deploy to validate persistence/volumes.
|
||||
- [ ] Backup/restore for Postgres & Mongo verified weekly (not automated here).
|
||||
- [ ] NATS JetStream retention + DLQ policy reviewed and documented.
|
||||
|
||||
## Escalation
|
||||
- Primary: Orchestrator on-call.
|
||||
- Secondary: DevOps Guild (release).
|
||||
- Page when any critical alert persists >15m or dual criticals fire simultaneously.
|
||||
124
devops/services/orchestrator/Dockerfile
Normal file
124
devops/services/orchestrator/Dockerfile
Normal file
@@ -0,0 +1,124 @@
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
|
||||
# Orchestrator Service Dockerfile
|
||||
# Multi-stage build for deterministic, reproducible container images.
|
||||
# Supports air-gapped deployment via digest-pinned base images.
|
||||
|
||||
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/nightly/sdk:10.0
|
||||
ARG RUNTIME_IMAGE=mcr.microsoft.com/dotnet/nightly/aspnet:10.0
|
||||
|
||||
ARG VERSION=0.0.0
|
||||
ARG CHANNEL=dev
|
||||
ARG GIT_SHA=0000000
|
||||
ARG SOURCE_DATE_EPOCH=0
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 1: Build
|
||||
# ==============================================================================
|
||||
FROM ${SDK_IMAGE} AS build
|
||||
ARG GIT_SHA
|
||||
ARG SOURCE_DATE_EPOCH
|
||||
WORKDIR /src
|
||||
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 \
|
||||
NUGET_XMLDOC_MODE=skip \
|
||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
|
||||
|
||||
# Copy solution and project files for restore
|
||||
COPY src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.sln ./
|
||||
COPY src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Core/StellaOps.Orchestrator.Core.csproj StellaOps.Orchestrator.Core/
|
||||
COPY src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Infrastructure/StellaOps.Orchestrator.Infrastructure.csproj StellaOps.Orchestrator.Infrastructure/
|
||||
COPY src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/StellaOps.Orchestrator.WebService.csproj StellaOps.Orchestrator.WebService/
|
||||
COPY src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Worker/StellaOps.Orchestrator.Worker.csproj StellaOps.Orchestrator.Worker/
|
||||
COPY Directory.Build.props Directory.Packages.props ./
|
||||
|
||||
# Restore dependencies with cache mount
|
||||
RUN --mount=type=cache,target=/root/.nuget/packages \
|
||||
dotnet restore StellaOps.Orchestrator.sln
|
||||
|
||||
# Copy source files
|
||||
COPY src/Orchestrator/StellaOps.Orchestrator/ ./
|
||||
|
||||
# Publish WebService
|
||||
RUN --mount=type=cache,target=/root/.nuget/packages \
|
||||
dotnet publish StellaOps.Orchestrator.WebService/StellaOps.Orchestrator.WebService.csproj \
|
||||
-c Release \
|
||||
-o /app/publish/webservice \
|
||||
/p:UseAppHost=false \
|
||||
/p:ContinuousIntegrationBuild=true \
|
||||
/p:SourceRevisionId=${GIT_SHA} \
|
||||
/p:Deterministic=true \
|
||||
/p:TreatWarningsAsErrors=true
|
||||
|
||||
# Publish Worker (optional, for hybrid deployments)
|
||||
RUN --mount=type=cache,target=/root/.nuget/packages \
|
||||
dotnet publish StellaOps.Orchestrator.Worker/StellaOps.Orchestrator.Worker.csproj \
|
||||
-c Release \
|
||||
-o /app/publish/worker \
|
||||
/p:UseAppHost=false \
|
||||
/p:ContinuousIntegrationBuild=true \
|
||||
/p:SourceRevisionId=${GIT_SHA} \
|
||||
/p:Deterministic=true \
|
||||
/p:TreatWarningsAsErrors=true
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 2: Runtime (WebService)
|
||||
# ==============================================================================
|
||||
FROM ${RUNTIME_IMAGE} AS orchestrator-web
|
||||
WORKDIR /app
|
||||
ARG VERSION
|
||||
ARG CHANNEL
|
||||
ARG GIT_SHA
|
||||
|
||||
ENV DOTNET_EnableDiagnostics=0 \
|
||||
ASPNETCORE_URLS=http://0.0.0.0:8080 \
|
||||
ASPNETCORE_ENVIRONMENT=Production \
|
||||
ORCHESTRATOR__TELEMETRY__MINIMUMLOGLEVEL=Information
|
||||
|
||||
COPY --from=build /app/publish/webservice/ ./
|
||||
|
||||
# Health check endpoints
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
LABEL org.opencontainers.image.title="StellaOps Orchestrator WebService" \
|
||||
org.opencontainers.image.description="Job scheduling, DAG planning, and worker coordination service" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${GIT_SHA}" \
|
||||
org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/stellaops" \
|
||||
org.opencontainers.image.vendor="StellaOps" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
org.stellaops.release.channel="${CHANNEL}" \
|
||||
org.stellaops.component="orchestrator-web"
|
||||
|
||||
ENTRYPOINT ["dotnet", "StellaOps.Orchestrator.WebService.dll"]
|
||||
|
||||
# ==============================================================================
|
||||
# Stage 3: Runtime (Worker)
|
||||
# ==============================================================================
|
||||
FROM ${RUNTIME_IMAGE} AS orchestrator-worker
|
||||
WORKDIR /app
|
||||
ARG VERSION
|
||||
ARG CHANNEL
|
||||
ARG GIT_SHA
|
||||
|
||||
ENV DOTNET_EnableDiagnostics=0 \
|
||||
ASPNETCORE_ENVIRONMENT=Production \
|
||||
ORCHESTRATOR__TELEMETRY__MINIMUMLOGLEVEL=Information
|
||||
|
||||
COPY --from=build /app/publish/worker/ ./
|
||||
|
||||
LABEL org.opencontainers.image.title="StellaOps Orchestrator Worker" \
|
||||
org.opencontainers.image.description="Background worker for job execution and orchestration tasks" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${GIT_SHA}" \
|
||||
org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/stellaops" \
|
||||
org.opencontainers.image.vendor="StellaOps" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
org.stellaops.release.channel="${CHANNEL}" \
|
||||
org.stellaops.component="orchestrator-worker"
|
||||
|
||||
ENTRYPOINT ["dotnet", "StellaOps.Orchestrator.Worker.dll"]
|
||||
108
devops/services/orchestrator/GA_CHECKLIST.md
Normal file
108
devops/services/orchestrator/GA_CHECKLIST.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Orchestrator Service GA Checklist
|
||||
|
||||
> Pre-release validation checklist for StellaOps Orchestrator Service.
|
||||
> All items must be verified before promoting to `stable` channel.
|
||||
|
||||
## Build & Packaging
|
||||
|
||||
- [ ] Container images build successfully for all target architectures (amd64, arm64)
|
||||
- [ ] Multi-stage Dockerfile produces minimal runtime images (<100MB compressed)
|
||||
- [ ] OCI labels include version, git SHA, and license metadata
|
||||
- [ ] HEALTHCHECK directive validates endpoint availability
|
||||
- [ ] Build is reproducible (same inputs produce byte-identical outputs)
|
||||
- [ ] SBOM generated and attached to container images (SPDX 3.0.1 or CycloneDX 1.6)
|
||||
- [ ] Provenance attestation generated per SLSA v1 specification
|
||||
- [ ] Air-gap bundle script creates valid offline deployment package
|
||||
|
||||
## Security
|
||||
|
||||
- [ ] Container runs as non-root user (UID 1000+)
|
||||
- [ ] No secrets baked into container image layers
|
||||
- [ ] Base image digest-pinned to known-good version
|
||||
- [ ] Vulnerability scan passes with no HIGH/CRITICAL unfixed CVEs
|
||||
- [ ] TLS 1.3 enforced for all external endpoints
|
||||
- [ ] Authority JWT validation enabled and tested
|
||||
- [ ] Tenant isolation enforced at API and storage layers
|
||||
- [ ] Sensitive configuration loaded from Kubernetes secrets only
|
||||
|
||||
## Functional
|
||||
|
||||
- [ ] Job scheduling CRUD operations work correctly
|
||||
- [ ] Cron expression parsing handles edge cases (DST, leap years)
|
||||
- [ ] DAG planning respects dependency ordering
|
||||
- [ ] Dead letter queue captures failed jobs with full context
|
||||
- [ ] Backfill API handles large date ranges without OOM
|
||||
- [ ] Worker heartbeat detection marks stale jobs correctly
|
||||
- [ ] Rate limiting and concurrency limits enforced per tenant
|
||||
|
||||
## Performance & Scale
|
||||
|
||||
- [ ] System tracks 10,000+ pending jobs without degradation
|
||||
- [ ] Dispatch latency P95 < 150ms under normal load
|
||||
- [ ] Queue depth metrics exposed for autoscaling (KEDA/HPA)
|
||||
- [ ] Load shedding activates at configured thresholds
|
||||
- [ ] Database connection pooling sized appropriately
|
||||
- [ ] Memory usage stable under sustained load (no leaks)
|
||||
|
||||
## Observability
|
||||
|
||||
- [ ] Structured logging with correlation IDs enabled
|
||||
- [ ] OpenTelemetry traces exported to configured endpoint
|
||||
- [ ] Prometheus metrics exposed at `/metrics` endpoint
|
||||
- [ ] Health probes respond correctly:
|
||||
- `/healthz` - basic liveness
|
||||
- `/livez` - deep liveness with dependency checks
|
||||
- `/readyz` - readiness for traffic
|
||||
- `/startupz` - startup completion check
|
||||
- [ ] Autoscaling metrics endpoint returns valid JSON
|
||||
|
||||
## Deployment
|
||||
|
||||
- [ ] Helm values overlay tested with production-like configuration
|
||||
- [ ] PostgreSQL schema migrations run idempotently
|
||||
- [ ] Rolling update strategy configured (maxSurge/maxUnavailable)
|
||||
- [ ] Pod disruption budget prevents full outage
|
||||
- [ ] Resource requests/limits appropriate for target workload
|
||||
- [ ] Network policies restrict traffic to required paths only
|
||||
- [ ] Service mesh (Istio/Linkerd) integration tested if applicable
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] Architecture document updated in `docs/modules/orchestrator/`
|
||||
- [ ] API reference generated from OpenAPI spec
|
||||
- [ ] Runbook for common operations (restart, scale, failover)
|
||||
- [ ] Troubleshooting guide for known issues
|
||||
- [ ] Upgrade path documented from previous versions
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Unit tests pass (100% of Core, 80%+ of Infrastructure)
|
||||
- [ ] Integration tests pass against real PostgreSQL
|
||||
- [ ] Performance benchmarks meet targets
|
||||
- [ ] Chaos testing validates graceful degradation
|
||||
- [ ] E2E tests cover critical user journeys
|
||||
|
||||
## Compliance
|
||||
|
||||
- [ ] AGPL-3.0-or-later license headers in all source files
|
||||
- [ ] Third-party license notices collected and bundled
|
||||
- [ ] Attestation chain verifiable via `stella attest verify`
|
||||
- [ ] Air-gap deployment tested in isolated network
|
||||
- [ ] CryptoProfile compatibility verified (FIPS/eIDAS if required)
|
||||
|
||||
---
|
||||
|
||||
## Sign-off
|
||||
|
||||
| Role | Name | Date | Signature |
|
||||
|------|------|------|-----------|
|
||||
| Engineering Lead | | | |
|
||||
| QA Lead | | | |
|
||||
| Security Review | | | |
|
||||
| Release Manager | | | |
|
||||
|
||||
**Release Version:** ________________
|
||||
|
||||
**Release Channel:** [ ] edge [ ] stable [ ] lts
|
||||
|
||||
**Notes:**
|
||||
276
devops/services/orchestrator/build-airgap-bundle.sh
Normal file
276
devops/services/orchestrator/build-airgap-bundle.sh
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ORCH-SVC-34-004: Build air-gap bundle for Orchestrator service
|
||||
# Packages container images, configs, and manifests for offline deployment.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
VERSION="${VERSION:-2025.10.0-edge}"
|
||||
CHANNEL="${CHANNEL:-edge}"
|
||||
BUNDLE_DIR="${BUNDLE_DIR:-$REPO_ROOT/out/bundles/orchestrator-${VERSION}}"
|
||||
SRC_DIR="${SRC_DIR:-$REPO_ROOT/out/buildx/orchestrator}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
|
||||
Build an air-gap bundle for StellaOps Orchestrator service.
|
||||
|
||||
Options:
|
||||
--version VERSION Bundle version (default: $VERSION)
|
||||
--channel CHANNEL Release channel (default: $CHANNEL)
|
||||
--output DIR Output bundle directory (default: $BUNDLE_DIR)
|
||||
--source DIR Source buildx directory (default: $SRC_DIR)
|
||||
--skip-images Skip OCI image export (use existing)
|
||||
--help Show this help
|
||||
|
||||
Environment variables:
|
||||
VERSION, CHANNEL, BUNDLE_DIR, SRC_DIR
|
||||
|
||||
Examples:
|
||||
$0 --version 2025.10.0 --channel stable
|
||||
VERSION=2025.10.0 CHANNEL=stable $0
|
||||
EOF
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
SKIP_IMAGES=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
--channel) CHANNEL="$2"; shift 2 ;;
|
||||
--output) BUNDLE_DIR="$2"; shift 2 ;;
|
||||
--source) SRC_DIR="$2"; shift 2 ;;
|
||||
--skip-images) SKIP_IMAGES=true; shift ;;
|
||||
--help) usage 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage 64 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
BUNDLE_DIR="${BUNDLE_DIR:-$REPO_ROOT/out/bundles/orchestrator-${VERSION}}"
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
echo "[orchestrator-airgap] Building bundle v${VERSION} (${CHANNEL})"
|
||||
echo "[orchestrator-airgap] Output: ${BUNDLE_DIR}"
|
||||
|
||||
mkdir -p "$BUNDLE_DIR"/{images,configs,manifests,docs}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Stage 1: Export container images as OCI archives
|
||||
# ------------------------------------------------------------------------------
|
||||
if [[ "$SKIP_IMAGES" == "false" ]]; then
|
||||
echo "[orchestrator-airgap] Exporting container images..."
|
||||
|
||||
IMAGES=(
|
||||
"orchestrator-web:${VERSION}"
|
||||
"orchestrator-worker:${VERSION}"
|
||||
)
|
||||
|
||||
for img in "${IMAGES[@]}"; do
|
||||
img_name="${img%%:*}"
|
||||
img_file="${BUNDLE_DIR}/images/${img_name}.oci.tar.gz"
|
||||
|
||||
if [[ -f "${SRC_DIR}/${img_name}/image.oci" ]]; then
|
||||
echo "[orchestrator-airgap] Packaging ${img_name} from buildx output..."
|
||||
gzip -c "${SRC_DIR}/${img_name}/image.oci" > "$img_file"
|
||||
else
|
||||
echo "[orchestrator-airgap] Exporting ${img_name} via docker save..."
|
||||
docker save "registry.stella-ops.org/stellaops/${img}" | gzip > "$img_file"
|
||||
fi
|
||||
|
||||
# Generate checksum
|
||||
sha256sum "$img_file" | cut -d' ' -f1 > "${img_file}.sha256"
|
||||
|
||||
# Copy SBOM if available
|
||||
if [[ -f "${SRC_DIR}/${img_name}/sbom.syft.json" ]]; then
|
||||
cp "${SRC_DIR}/${img_name}/sbom.syft.json" "${BUNDLE_DIR}/manifests/${img_name}.sbom.json"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "[orchestrator-airgap] Skipping image export (--skip-images)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Stage 2: Copy configuration templates
|
||||
# ------------------------------------------------------------------------------
|
||||
echo "[orchestrator-airgap] Copying configuration templates..."
|
||||
|
||||
# Helm values overlay
|
||||
if [[ -f "$REPO_ROOT/deploy/helm/stellaops/values-orchestrator.yaml" ]]; then
|
||||
cp "$REPO_ROOT/deploy/helm/stellaops/values-orchestrator.yaml" \
|
||||
"${BUNDLE_DIR}/configs/values-orchestrator.yaml"
|
||||
fi
|
||||
|
||||
# Sample configuration
|
||||
if [[ -f "$REPO_ROOT/etc/orchestrator.yaml.sample" ]]; then
|
||||
cp "$REPO_ROOT/etc/orchestrator.yaml.sample" \
|
||||
"${BUNDLE_DIR}/configs/orchestrator.yaml.sample"
|
||||
fi
|
||||
|
||||
# PostgreSQL migration scripts
|
||||
if [[ -d "$REPO_ROOT/src/Orchestrator/StellaOps.Orchestrator/migrations" ]]; then
|
||||
mkdir -p "${BUNDLE_DIR}/configs/migrations"
|
||||
cp "$REPO_ROOT/src/Orchestrator/StellaOps.Orchestrator/migrations/"*.sql \
|
||||
"${BUNDLE_DIR}/configs/migrations/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Bootstrap secrets template
|
||||
cat > "${BUNDLE_DIR}/configs/secrets.env.example" <<'SECRETS_EOF'
|
||||
# Orchestrator Secrets Template
|
||||
# Copy to secrets.env and fill in values before deployment
|
||||
|
||||
# PostgreSQL password (required)
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Authority JWT signing key (if using local Authority)
|
||||
AUTHORITY_SIGNING_KEY=
|
||||
|
||||
# OpenTelemetry endpoint (optional)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
|
||||
# Tenant encryption key for multi-tenant isolation (optional)
|
||||
TENANT_ENCRYPTION_KEY=
|
||||
SECRETS_EOF
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Stage 3: Generate bundle manifest
|
||||
# ------------------------------------------------------------------------------
|
||||
echo "[orchestrator-airgap] Generating bundle manifest..."
|
||||
|
||||
# Calculate checksums for all bundle files
|
||||
MANIFEST_FILE="${BUNDLE_DIR}/manifests/bundle-manifest.json"
|
||||
|
||||
# Build file list with checksums
|
||||
FILES_JSON="[]"
|
||||
while IFS= read -r -d '' file; do
|
||||
rel_path="${file#$BUNDLE_DIR/}"
|
||||
if [[ "$rel_path" != "manifests/bundle-manifest.json" ]]; then
|
||||
sha=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo "0")
|
||||
FILES_JSON=$(echo "$FILES_JSON" | jq --arg name "$rel_path" --arg sha "$sha" --arg size "$size" \
|
||||
'. + [{"name": $name, "sha256": $sha, "size": ($size | tonumber)}]')
|
||||
fi
|
||||
done < <(find "$BUNDLE_DIR" -type f -print0 | sort -z)
|
||||
|
||||
cat > "$MANIFEST_FILE" <<EOF
|
||||
{
|
||||
"bundle": {
|
||||
"name": "stellaops-orchestrator",
|
||||
"version": "${VERSION}",
|
||||
"channel": "${CHANNEL}",
|
||||
"createdAt": "${TIMESTAMP}",
|
||||
"components": [
|
||||
{
|
||||
"name": "orchestrator-web",
|
||||
"type": "container",
|
||||
"image": "registry.stella-ops.org/stellaops/orchestrator-web:${VERSION}"
|
||||
},
|
||||
{
|
||||
"name": "orchestrator-worker",
|
||||
"type": "container",
|
||||
"image": "registry.stella-ops.org/stellaops/orchestrator-worker:${VERSION}"
|
||||
},
|
||||
{
|
||||
"name": "orchestrator-postgres",
|
||||
"type": "infrastructure",
|
||||
"image": "docker.io/library/postgres:16-alpine"
|
||||
}
|
||||
]
|
||||
},
|
||||
"files": ${FILES_JSON}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Checksum the manifest itself
|
||||
sha256sum "$MANIFEST_FILE" | cut -d' ' -f1 > "${MANIFEST_FILE}.sha256"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Stage 4: Copy documentation
|
||||
# ------------------------------------------------------------------------------
|
||||
echo "[orchestrator-airgap] Copying documentation..."
|
||||
|
||||
# Module architecture
|
||||
if [[ -f "$REPO_ROOT/docs/modules/orchestrator/architecture.md" ]]; then
|
||||
cp "$REPO_ROOT/docs/modules/orchestrator/architecture.md" \
|
||||
"${BUNDLE_DIR}/docs/architecture.md"
|
||||
fi
|
||||
|
||||
# GA checklist
|
||||
if [[ -f "$REPO_ROOT/ops/orchestrator/GA_CHECKLIST.md" ]]; then
|
||||
cp "$REPO_ROOT/ops/orchestrator/GA_CHECKLIST.md" \
|
||||
"${BUNDLE_DIR}/docs/GA_CHECKLIST.md"
|
||||
fi
|
||||
|
||||
# Quick deployment guide
|
||||
cat > "${BUNDLE_DIR}/docs/DEPLOY.md" <<'DEPLOY_EOF'
|
||||
# Orchestrator Air-Gap Deployment Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker or containerd runtime
|
||||
- Kubernetes 1.28+ (for Helm deployment) or Docker Compose
|
||||
- PostgreSQL 16+ (included as container or external)
|
||||
|
||||
## Quick Start (Docker)
|
||||
|
||||
1. Load images:
|
||||
```bash
|
||||
for img in images/*.oci.tar.gz; do
|
||||
gunzip -c "$img" | docker load
|
||||
done
|
||||
```
|
||||
|
||||
2. Configure secrets:
|
||||
```bash
|
||||
cp configs/secrets.env.example secrets.env
|
||||
# Edit secrets.env with your values
|
||||
```
|
||||
|
||||
3. Start services:
|
||||
```bash
|
||||
docker compose -f docker-compose.orchestrator.yaml up -d
|
||||
```
|
||||
|
||||
## Helm Deployment
|
||||
|
||||
1. Import images to registry:
|
||||
```bash
|
||||
for img in images/*.oci.tar.gz; do
|
||||
crane push "$img" your-registry.local/stellaops/$(basename "$img" .oci.tar.gz)
|
||||
done
|
||||
```
|
||||
|
||||
2. Install chart:
|
||||
```bash
|
||||
helm upgrade --install stellaops ./stellaops \
|
||||
-f configs/values-orchestrator.yaml \
|
||||
--set global.imageRegistry=your-registry.local
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Check health endpoints:
|
||||
```bash
|
||||
curl http://localhost:8080/healthz
|
||||
curl http://localhost:8080/readyz
|
||||
```
|
||||
DEPLOY_EOF
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Stage 5: Create final tarball
|
||||
# ------------------------------------------------------------------------------
|
||||
echo "[orchestrator-airgap] Creating final tarball..."
|
||||
|
||||
TARBALL="${BUNDLE_DIR}.tar.gz"
|
||||
tar -C "$(dirname "$BUNDLE_DIR")" -czf "$TARBALL" "$(basename "$BUNDLE_DIR")"
|
||||
|
||||
# Checksum the tarball
|
||||
sha256sum "$TARBALL" | cut -d' ' -f1 > "${TARBALL}.sha256"
|
||||
|
||||
echo "[orchestrator-airgap] Bundle created successfully:"
|
||||
echo " Tarball: ${TARBALL}"
|
||||
echo " SHA256: $(cat "${TARBALL}.sha256")"
|
||||
echo " Size: $(du -h "$TARBALL" | cut -f1)"
|
||||
106
devops/services/orchestrator/provenance.json
Normal file
106
devops/services/orchestrator/provenance.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "registry.stella-ops.org/stellaops/orchestrator-web",
|
||||
"digest": {
|
||||
"sha256": "<IMAGE_DIGEST_WEB>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "registry.stella-ops.org/stellaops/orchestrator-worker",
|
||||
"digest": {
|
||||
"sha256": "<IMAGE_DIGEST_WORKER>"
|
||||
}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://slsa.dev/provenance/v1",
|
||||
"predicate": {
|
||||
"buildDefinition": {
|
||||
"buildType": "https://stella-ops.org/OrchestratorBuild/v1",
|
||||
"externalParameters": {
|
||||
"source": {
|
||||
"uri": "git+https://git.stella-ops.org/stella-ops/stellaops.git",
|
||||
"digest": {
|
||||
"gitCommit": "<GIT_SHA>"
|
||||
}
|
||||
},
|
||||
"builderImage": {
|
||||
"uri": "mcr.microsoft.com/dotnet/nightly/sdk:10.0",
|
||||
"digest": {
|
||||
"sha256": "<SDK_DIGEST>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internalParameters": {
|
||||
"dockerfile": "ops/orchestrator/Dockerfile",
|
||||
"targetStages": ["orchestrator-web", "orchestrator-worker"],
|
||||
"buildArgs": {
|
||||
"VERSION": "<VERSION>",
|
||||
"CHANNEL": "<CHANNEL>",
|
||||
"GIT_SHA": "<GIT_SHA>",
|
||||
"SOURCE_DATE_EPOCH": "<SOURCE_DATE_EPOCH>"
|
||||
}
|
||||
},
|
||||
"resolvedDependencies": [
|
||||
{
|
||||
"uri": "pkg:nuget/Microsoft.Extensions.Hosting@10.0.0",
|
||||
"digest": {
|
||||
"sha256": "<NUGET_HOSTING_DIGEST>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uri": "pkg:nuget/Npgsql.EntityFrameworkCore.PostgreSQL@10.0.0",
|
||||
"digest": {
|
||||
"sha256": "<NUGET_NPGSQL_DIGEST>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uri": "pkg:nuget/Cronos@0.10.0",
|
||||
"digest": {
|
||||
"sha256": "<NUGET_CRONOS_DIGEST>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"runDetails": {
|
||||
"builder": {
|
||||
"id": "https://git.stella-ops.org/stella-ops/stellaops/-/runners/1",
|
||||
"builderDependencies": [
|
||||
{
|
||||
"uri": "docker.io/moby/buildkit:latest",
|
||||
"digest": {
|
||||
"sha256": "<BUILDKIT_DIGEST>"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"buildkit": "0.14.0"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"invocationId": "<INVOCATION_ID>",
|
||||
"startedOn": "<BUILD_START_TIME>",
|
||||
"finishedOn": "<BUILD_END_TIME>"
|
||||
},
|
||||
"byproducts": [
|
||||
{
|
||||
"name": "sbom-web",
|
||||
"uri": "registry.stella-ops.org/stellaops/orchestrator-web:sbom",
|
||||
"mediaType": "application/spdx+json",
|
||||
"digest": {
|
||||
"sha256": "<SBOM_WEB_DIGEST>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sbom-worker",
|
||||
"uri": "registry.stella-ops.org/stellaops/orchestrator-worker:sbom",
|
||||
"mediaType": "application/spdx+json",
|
||||
"digest": {
|
||||
"sha256": "<SBOM_WORKER_DIGEST>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
29
devops/services/sbom-ci-runner/README.md
Normal file
29
devops/services/sbom-ci-runner/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# SBOM Service CI Runner Harness (DEVOPS-SBOM-23-001)
|
||||
|
||||
Purpose: deterministic, offline-friendly CI harness for SBOM Service. Produces warmed-cache restore, build binlog, TRX outputs, and a NuGet cache hash to unblock SBOM console/consumer sprints.
|
||||
|
||||
Usage
|
||||
- From repo root run: `ops/devops/sbom-ci-runner/run-sbom-ci.sh`
|
||||
- Outputs land in `ops/devops/artifacts/sbom-ci/<UTC timestamp>/`:
|
||||
- `build.binlog` (solution build)
|
||||
- `tests/sbom.trx` (VSTest results)
|
||||
- `nuget-cache.hash` (sha256 over file name+size listing for offline cache traceability)
|
||||
- `summary.json` (paths + sources + cache hash)
|
||||
|
||||
Environment defaults
|
||||
- `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1`, `DOTNET_RESTORE_DISABLE_PARALLEL=1`
|
||||
- `NUGET_PACKAGES=$REPO/.nuget/packages`
|
||||
- `NUGET_SOURCES=$REPO/local-nugets;$REPO/.nuget/packages`
|
||||
- `TEST_FILTER` empty (set to narrow tests)
|
||||
|
||||
What it does
|
||||
1) Warm NuGet cache from `local-nugets/` into `$NUGET_PACKAGES` for air-gap parity.
|
||||
2) `dotnet restore` + `dotnet build` on `src/SbomService/StellaOps.SbomService.sln` with `/bl`.
|
||||
3) Run `StellaOps.SbomService.Tests` with TRX output (honors `TEST_FILTER`).
|
||||
4) Produce `nuget-cache.hash` using sorted file name+size list hashed with sha256 (lightweight evidence of cache contents).
|
||||
5) Emit `summary.json` with artefact paths and cache hash value.
|
||||
|
||||
Notes
|
||||
- Offline-only; no external services required.
|
||||
- Timestamped output folders keep ordering deterministic; consumers should sort lexicographically.
|
||||
- Extend `test_project` in the script if additional SBOM test projects are added.
|
||||
72
devops/services/sbom-ci-runner/run-sbom-ci.sh
Normal file
72
devops/services/sbom-ci-runner/run-sbom-ci.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# SBOM Service CI runner (DEVOPS-SBOM-23-001)
|
||||
# Builds SBOM solution and runs tests with warmed NuGet cache; emits binlog + TRX + cache hash summary.
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
out_dir="$repo_root/ops/devops/artifacts/sbom-ci/$ts"
|
||||
logs_dir="$out_dir/tests"
|
||||
mkdir -p "$logs_dir"
|
||||
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT:-1}
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=${DOTNET_SKIP_FIRST_TIME_EXPERIENCE:-1}
|
||||
export DOTNET_RESTORE_DISABLE_PARALLEL=${DOTNET_RESTORE_DISABLE_PARALLEL:-1}
|
||||
export NUGET_PACKAGES=${NUGET_PACKAGES:-$repo_root/.nuget/packages}
|
||||
export NUGET_SOURCES=${NUGET_SOURCES:-"$repo_root/local-nugets;$repo_root/.nuget/packages"}
|
||||
export TEST_FILTER=${TEST_FILTER:-""}
|
||||
|
||||
mkdir -p "$NUGET_PACKAGES"
|
||||
rsync -a "$repo_root/local-nugets/" "$NUGET_PACKAGES/" >/dev/null 2>&1 || true
|
||||
|
||||
restore_sources=()
|
||||
IFS=';' read -ra SRC_ARR <<< "$NUGET_SOURCES"
|
||||
for s in "${SRC_ARR[@]}"; do
|
||||
[[ -n "$s" ]] && restore_sources+=(--source "$s")
|
||||
done
|
||||
|
||||
solution="$repo_root/src/SbomService/StellaOps.SbomService.sln"
|
||||
dotnet restore "$solution" --ignore-failed-sources "${restore_sources[@]}"
|
||||
|
||||
build_binlog="$out_dir/build.binlog"
|
||||
dotnet build "$solution" -c Release /p:ContinuousIntegrationBuild=true /bl:"$build_binlog"
|
||||
|
||||
trx_name="sbom.trx"
|
||||
test_project="$repo_root/src/SbomService/StellaOps.SbomService.Tests/StellaOps.SbomService.Tests.csproj"
|
||||
common_test_args=( -c Release --no-build --results-directory "$logs_dir" )
|
||||
if [[ -n "$TEST_FILTER" ]]; then
|
||||
common_test_args+=( --filter "$TEST_FILTER" )
|
||||
fi
|
||||
|
||||
if [[ -f "$test_project" ]]; then
|
||||
dotnet test "$test_project" "${common_test_args[@]}" --logger "trx;LogFileName=$trx_name"
|
||||
fi
|
||||
|
||||
# Lightweight cache hash: list files with size, hash the listing
|
||||
cache_listing="$out_dir/nuget-cache.list"
|
||||
find "$NUGET_PACKAGES" -type f -printf "%P %s\n" | sort > "$cache_listing"
|
||||
cache_hash=$(sha256sum "$cache_listing" | awk '{print $1}')
|
||||
|
||||
echo "$cache_hash nuget-cache.list" > "$out_dir/nuget-cache.hash"
|
||||
|
||||
summary="$out_dir/summary.json"
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "timestamp_utc": "%s",\n' "$ts"
|
||||
printf ' "build_binlog": "%s",\n' "${build_binlog#${repo_root}/}"
|
||||
printf ' "tests": [\n'
|
||||
printf ' {"project":"SbomService","trx":"%s"}\n' "${logs_dir#${repo_root}/}/$trx_name"
|
||||
printf ' ],\n'
|
||||
printf ' "nuget_packages": "%s",\n' "${NUGET_PACKAGES#${repo_root}/}"
|
||||
printf ' "cache_hash": "%s",\n' "$cache_hash"
|
||||
printf ' "sources": [\n'
|
||||
for i in "${!SRC_ARR[@]}"; do
|
||||
sep=","; [[ $i -eq $((${#SRC_ARR[@]}-1)) ]] && sep=""
|
||||
printf ' "%s"%s\n' "${SRC_ARR[$i]}" "$sep"
|
||||
done
|
||||
printf ' ]\n'
|
||||
printf '}\n'
|
||||
} > "$summary"
|
||||
|
||||
echo "Artifacts written to ${out_dir#${repo_root}/}"
|
||||
25
devops/services/scanner-ci-runner/README.md
Normal file
25
devops/services/scanner-ci-runner/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Scanner CI Runner Harness (DEVOPS-SCANNER-CI-11-001)
|
||||
|
||||
Purpose: deterministic, offline-friendly harness that restores, builds, and exercises the Scanner analyzers + WebService/Worker tests with warmed NuGet cache and TRX/binlog outputs.
|
||||
|
||||
Usage
|
||||
- From repo root run: `ops/devops/scanner-ci-runner/run-scanner-ci.sh`
|
||||
- Outputs land in `ops/devops/artifacts/scanner-ci/<UTC timestamp>/`:
|
||||
- `build.binlog` (solution build)
|
||||
- `tests/*.trx` for grouped test runs
|
||||
- `summary.json` listing artefact paths and SHA256s
|
||||
|
||||
Environment
|
||||
- Defaults: `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1`, `NUGET_PACKAGES=$REPO/.nuget/packages`.
|
||||
- Sources: `NUGET_SOURCES` (semicolon-separated) defaults to `local-nugets` then warmed cache; no internet required when cache is primed.
|
||||
- `TEST_FILTER` can narrow tests (empty = all).
|
||||
|
||||
What it does
|
||||
1) Warm NuGet cache from `local-nugets/` into `$NUGET_PACKAGES`.
|
||||
2) `dotnet restore` + `dotnet build` on `src/Scanner/StellaOps.Scanner.sln` with `/bl`.
|
||||
3) Run Scanner test buckets (core/analyzers/web/worker) with TRX outputs; buckets can be adjusted via `TEST_FILTER` or script edits.
|
||||
4) Emit `summary.json` with artefact paths/hashes for reproducibility.
|
||||
|
||||
Notes
|
||||
- Buckets are ordered to keep runtime predictable; adjust filters to target a subset when iterating.
|
||||
- Timestamped output directories keep ordering deterministic in offline pipelines.
|
||||
88
devops/services/scanner-ci-runner/run-scanner-ci.sh
Normal file
88
devops/services/scanner-ci-runner/run-scanner-ci.sh
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Scanner CI runner harness (DEVOPS-SCANNER-CI-11-001)
|
||||
# Builds Scanner solution and runs grouped test buckets with warmed NuGet cache.
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
out_dir="$repo_root/ops/devops/artifacts/scanner-ci/$ts"
|
||||
logs_dir="$out_dir/tests"
|
||||
mkdir -p "$logs_dir"
|
||||
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT:-1}
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=${DOTNET_SKIP_FIRST_TIME_EXPERIENCE:-1}
|
||||
export NUGET_PACKAGES=${NUGET_PACKAGES:-$repo_root/.nuget/packages}
|
||||
export NUGET_SOURCES=${NUGET_SOURCES:-"$repo_root/local-nugets;$repo_root/.nuget/packages"}
|
||||
export TEST_FILTER=${TEST_FILTER:-""}
|
||||
export DOTNET_RESTORE_DISABLE_PARALLEL=${DOTNET_RESTORE_DISABLE_PARALLEL:-1}
|
||||
|
||||
mkdir -p "$NUGET_PACKAGES"
|
||||
rsync -a "$repo_root/local-nugets/" "$NUGET_PACKAGES/" >/dev/null 2>&1 || true
|
||||
|
||||
restore_sources=()
|
||||
IFS=';' read -ra SRC_ARR <<< "$NUGET_SOURCES"
|
||||
for s in "${SRC_ARR[@]}"; do
|
||||
[[ -n "$s" ]] && restore_sources+=(--source "$s")
|
||||
done
|
||||
|
||||
solution="$repo_root/src/Scanner/StellaOps.Scanner.sln"
|
||||
dotnet restore "$solution" --ignore-failed-sources "${restore_sources[@]}"
|
||||
|
||||
build_binlog="$out_dir/build.binlog"
|
||||
dotnet build "$solution" -c Release /p:ContinuousIntegrationBuild=true /bl:"$build_binlog"
|
||||
|
||||
common_test_args=( -c Release --no-build --results-directory "$logs_dir" )
|
||||
if [[ -n "$TEST_FILTER" ]]; then
|
||||
common_test_args+=( --filter "$TEST_FILTER" )
|
||||
fi
|
||||
|
||||
run_tests() {
|
||||
local project="$1" name="$2"
|
||||
dotnet test "$project" "${common_test_args[@]}" --logger "trx;LogFileName=${name}.trx"
|
||||
}
|
||||
|
||||
run_tests "$repo_root/src/Scanner/__Tests/StellaOps.Scanner.Core.Tests/StellaOps.Scanner.Core.Tests.csproj" core
|
||||
run_tests "$repo_root/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.OS.Tests/StellaOps.Scanner.Analyzers.OS.Tests.csproj" analyzers-os
|
||||
run_tests "$repo_root/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Tests/StellaOps.Scanner.Analyzers.Lang.Tests.csproj" analyzers-lang
|
||||
run_tests "$repo_root/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj" web
|
||||
run_tests "$repo_root/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.csproj" worker
|
||||
|
||||
summary="$out_dir/summary.json"
|
||||
{
|
||||
printf '{
|
||||
'
|
||||
printf ' "timestamp_utc": "%s",
|
||||
' "$ts"
|
||||
printf ' "build_binlog": "%s",
|
||||
' "${build_binlog#${repo_root}/}"
|
||||
printf ' "tests": [
|
||||
'
|
||||
printf ' {"name":"core","trx":"%s"},
|
||||
' "${logs_dir#${repo_root}/}/core.trx"
|
||||
printf ' {"name":"analyzers-os","trx":"%s"},
|
||||
' "${logs_dir#${repo_root}/}/analyzers-os.trx"
|
||||
printf ' {"name":"analyzers-lang","trx":"%s"},
|
||||
' "${logs_dir#${repo_root}/}/analyzers-lang.trx"
|
||||
printf ' {"name":"web","trx":"%s"},
|
||||
' "${logs_dir#${repo_root}/}/web.trx"
|
||||
printf ' {"name":"worker","trx":"%s"}
|
||||
' "${logs_dir#${repo_root}/}/worker.trx"
|
||||
printf ' ],
|
||||
'
|
||||
printf ' "nuget_packages": "%s",
|
||||
' "${NUGET_PACKAGES#${repo_root}/}"
|
||||
printf ' "sources": [
|
||||
'
|
||||
for i in "${!SRC_ARR[@]}"; do
|
||||
sep=","; [[ $i -eq $((${#SRC_ARR[@]}-1)) ]] && sep=""
|
||||
printf ' "%s"%s
|
||||
' "${SRC_ARR[$i]}" "$sep"
|
||||
done
|
||||
printf ' ]
|
||||
'
|
||||
printf '}
|
||||
'
|
||||
} > "$summary"
|
||||
|
||||
echo "Artifacts written to ${out_dir#${repo_root}/}"
|
||||
89
devops/services/scanner-java/package-analyzer.sh
Normal file
89
devops/services/scanner-java/package-analyzer.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# Package Java analyzer plugin for release/offline distribution
|
||||
# Usage: ./package-analyzer.sh [version] [output-dir]
|
||||
# Example: ./package-analyzer.sh 2025.10.0 ./dist
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
VERSION="${1:-$(date +%Y.%m.%d)}"
|
||||
OUTPUT_DIR="${2:-${SCRIPT_DIR}/../artifacts/scanner-java}"
|
||||
PROJECT_PATH="src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/StellaOps.Scanner.Analyzers.Lang.Java.csproj"
|
||||
|
||||
# Freeze timestamps for reproducibility
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1704067200}
|
||||
|
||||
echo "==> Packaging Java analyzer v${VERSION}"
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
# Build for all target RIDs
|
||||
RIDS=("linux-x64" "linux-arm64" "osx-x64" "osx-arm64" "win-x64")
|
||||
|
||||
for RID in "${RIDS[@]}"; do
|
||||
echo "==> Building for ${RID}..."
|
||||
dotnet publish "${REPO_ROOT}/${PROJECT_PATH}" \
|
||||
--configuration Release \
|
||||
--runtime "${RID}" \
|
||||
--self-contained false \
|
||||
--output "${OUTPUT_DIR}/java-analyzer-${VERSION}-${RID}" \
|
||||
/p:Version="${VERSION}" \
|
||||
/p:PublishTrimmed=false \
|
||||
/p:DebugType=None
|
||||
done
|
||||
|
||||
# Create combined archive
|
||||
ARCHIVE_NAME="scanner-java-analyzer-${VERSION}"
|
||||
echo "==> Creating archive ${ARCHIVE_NAME}.tar.gz..."
|
||||
cd "${OUTPUT_DIR}"
|
||||
tar -czf "${ARCHIVE_NAME}.tar.gz" java-analyzer-${VERSION}-*/
|
||||
|
||||
# Generate checksums
|
||||
echo "==> Generating checksums..."
|
||||
sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
|
||||
for RID in "${RIDS[@]}"; do
|
||||
(cd "java-analyzer-${VERSION}-${RID}" && sha256sum *.dll *.json 2>/dev/null > ../java-analyzer-${VERSION}-${RID}.sha256 || true)
|
||||
done
|
||||
|
||||
# Generate SBOM if syft available
|
||||
if command -v syft &>/dev/null; then
|
||||
echo "==> Generating SBOM..."
|
||||
syft dir:"${OUTPUT_DIR}/java-analyzer-${VERSION}-linux-x64" -o spdx-json > "${OUTPUT_DIR}/${ARCHIVE_NAME}.spdx.json"
|
||||
syft dir:"${OUTPUT_DIR}/java-analyzer-${VERSION}-linux-x64" -o cyclonedx-json > "${OUTPUT_DIR}/${ARCHIVE_NAME}.cdx.json"
|
||||
fi
|
||||
|
||||
# Sign if cosign available
|
||||
if command -v cosign &>/dev/null && [[ -n "${COSIGN_KEY:-}" ]]; then
|
||||
echo "==> Signing archive..."
|
||||
cosign sign-blob --key "${COSIGN_KEY}" "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sig"
|
||||
fi
|
||||
|
||||
# Create manifest
|
||||
cat > "${OUTPUT_DIR}/manifest.json" <<EOF
|
||||
{
|
||||
"analyzer": "scanner-java",
|
||||
"version": "${VERSION}",
|
||||
"archive": "${ARCHIVE_NAME}.tar.gz",
|
||||
"checksumFile": "${ARCHIVE_NAME}.tar.gz.sha256",
|
||||
"rids": $(printf '%s\n' "${RIDS[@]}" | jq -R . | jq -s .),
|
||||
"sbom": {
|
||||
"spdx": "${ARCHIVE_NAME}.spdx.json",
|
||||
"cyclonedx": "${ARCHIVE_NAME}.cdx.json"
|
||||
},
|
||||
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"sourceDateEpoch": "${SOURCE_DATE_EPOCH}",
|
||||
"components": [
|
||||
"Maven/Gradle parsing",
|
||||
"JAR/WAR/EAR analysis",
|
||||
"Java callgraph builder",
|
||||
"JNI native bridge detection",
|
||||
"Service provider scanning",
|
||||
"Shaded JAR detection"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> Java analyzer packaged to ${OUTPUT_DIR}"
|
||||
echo " Archive: ${ARCHIVE_NAME}.tar.gz"
|
||||
echo " RIDs: ${RIDS[*]}"
|
||||
48
devops/services/scanner-java/release-plan.md
Normal file
48
devops/services/scanner-java/release-plan.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Java Analyzer Release Plan (DEVOPS-SCANNER-JAVA-21-011-REL)
|
||||
|
||||
## Goal
|
||||
Publish the Java analyzer plug-in with signed artifacts and offline-ready bundles for CLI/Offline Kit.
|
||||
|
||||
## Inputs
|
||||
- Analyzer JAR(s) + native helpers from dev task 21-011.
|
||||
- SBOM (SPDX JSON) for plugin + native components.
|
||||
- Test suite outputs (unit + integration).
|
||||
|
||||
## Artifacts
|
||||
- OCI image (optional) or zip bundle containing:
|
||||
- `analyzer.jar`
|
||||
- `lib/` natives (if any)
|
||||
- `LICENSE`, `NOTICE`
|
||||
- `SBOM` (spdx.json)
|
||||
- `SIGNATURES` (cosign/PGP)
|
||||
- Cosign attestations for OCI/zip (provenance + SBOM).
|
||||
- Checksums: `SHA256SUMS`, `SHA256SUMS.sig`.
|
||||
- Offline kit slice: tarball with bundle + attestations + SBOM.
|
||||
|
||||
## Pipeline steps
|
||||
1) **Build**: run gradle/mvn with `--offline` using vendored deps; produce JAR + natives.
|
||||
2) **SBOM**: `syft packages -o spdx-json` over build output.
|
||||
3) **Package**: zip bundle with fixed ordering (`zip -X`) and normalized timestamps (`SOURCE_DATE_EPOCH`).
|
||||
4) **Sign**:
|
||||
- cosign sign blob (zip) and/or image.
|
||||
- generate in-toto provenance (SLSA level 1) referencing git commit + toolchain hashes.
|
||||
5) **Checksums**: create `SHA256SUMS` and sign with cosign/PGP.
|
||||
6) **Verify stage**: pipeline step runs `cosign verify-blob`, `sha256sum --check`, and `syft validate spdx`.
|
||||
7) **Publish**:
|
||||
- Upload to artifact store (release bucket) with metadata (version, commit, digest).
|
||||
- Produce offline kit slice tarball (`scanner-java-<ver>-offline.tgz`) containing bundle, SBOM, attestations, checksums.
|
||||
|
||||
## Security/hardening
|
||||
- Non-root build container; disable gradle/mvn network (`--offline`).
|
||||
- Strip debug info unless required; ensure reproducible JAR (sorted entries, normalized timestamps).
|
||||
- Telemetry disabled.
|
||||
|
||||
## Evidence to capture
|
||||
- Bundle SHA256, cosign signatures, provenance statement.
|
||||
- SBOM hash.
|
||||
- Verification logs from pipeline.
|
||||
|
||||
## Owners
|
||||
- Build/pipeline: DevOps Guild
|
||||
- Signing policy: Platform Security
|
||||
- Consumer integration: CLI Guild / Offline Kit Guild
|
||||
87
devops/services/scanner-native/package-analyzer.sh
Normal file
87
devops/services/scanner-native/package-analyzer.sh
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Package Native analyzer plugin for release/offline distribution
|
||||
# Usage: ./package-analyzer.sh [version] [output-dir]
|
||||
# Example: ./package-analyzer.sh 2025.10.0 ./dist
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
VERSION="${1:-$(date +%Y.%m.%d)}"
|
||||
OUTPUT_DIR="${2:-${SCRIPT_DIR}/../artifacts/scanner-native}"
|
||||
PROJECT_PATH="src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj"
|
||||
|
||||
# Freeze timestamps for reproducibility
|
||||
export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1704067200}
|
||||
|
||||
echo "==> Packaging Native analyzer v${VERSION}"
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
# Build for all target RIDs
|
||||
RIDS=("linux-x64" "linux-arm64" "osx-x64" "osx-arm64" "win-x64")
|
||||
|
||||
for RID in "${RIDS[@]}"; do
|
||||
echo "==> Building for ${RID}..."
|
||||
dotnet publish "${REPO_ROOT}/${PROJECT_PATH}" \
|
||||
--configuration Release \
|
||||
--runtime "${RID}" \
|
||||
--self-contained false \
|
||||
--output "${OUTPUT_DIR}/native-analyzer-${VERSION}-${RID}" \
|
||||
/p:Version="${VERSION}" \
|
||||
/p:PublishTrimmed=false \
|
||||
/p:DebugType=None
|
||||
done
|
||||
|
||||
# Create combined archive
|
||||
ARCHIVE_NAME="scanner-native-analyzer-${VERSION}"
|
||||
echo "==> Creating archive ${ARCHIVE_NAME}.tar.gz..."
|
||||
cd "${OUTPUT_DIR}"
|
||||
tar -czf "${ARCHIVE_NAME}.tar.gz" native-analyzer-${VERSION}-*/
|
||||
|
||||
# Generate checksums
|
||||
echo "==> Generating checksums..."
|
||||
sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
|
||||
for RID in "${RIDS[@]}"; do
|
||||
(cd "native-analyzer-${VERSION}-${RID}" && sha256sum *.dll *.json 2>/dev/null > ../native-analyzer-${VERSION}-${RID}.sha256 || true)
|
||||
done
|
||||
|
||||
# Generate SBOM if syft available
|
||||
if command -v syft &>/dev/null; then
|
||||
echo "==> Generating SBOM..."
|
||||
syft dir:"${OUTPUT_DIR}/native-analyzer-${VERSION}-linux-x64" -o spdx-json > "${OUTPUT_DIR}/${ARCHIVE_NAME}.spdx.json"
|
||||
syft dir:"${OUTPUT_DIR}/native-analyzer-${VERSION}-linux-x64" -o cyclonedx-json > "${OUTPUT_DIR}/${ARCHIVE_NAME}.cdx.json"
|
||||
fi
|
||||
|
||||
# Sign if cosign available
|
||||
if command -v cosign &>/dev/null && [[ -n "${COSIGN_KEY:-}" ]]; then
|
||||
echo "==> Signing archive..."
|
||||
cosign sign-blob --key "${COSIGN_KEY}" "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sig"
|
||||
fi
|
||||
|
||||
# Create manifest
|
||||
cat > "${OUTPUT_DIR}/manifest.json" <<EOF
|
||||
{
|
||||
"analyzer": "scanner-native",
|
||||
"version": "${VERSION}",
|
||||
"archive": "${ARCHIVE_NAME}.tar.gz",
|
||||
"checksumFile": "${ARCHIVE_NAME}.tar.gz.sha256",
|
||||
"rids": $(printf '%s\n' "${RIDS[@]}" | jq -R . | jq -s .),
|
||||
"sbom": {
|
||||
"spdx": "${ARCHIVE_NAME}.spdx.json",
|
||||
"cyclonedx": "${ARCHIVE_NAME}.cdx.json"
|
||||
},
|
||||
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"sourceDateEpoch": "${SOURCE_DATE_EPOCH}",
|
||||
"components": [
|
||||
"ELF/PE/Mach-O parsing",
|
||||
"Symbol demangling (Itanium/Rust)",
|
||||
"Native callgraph builder",
|
||||
"Build-ID extraction"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> Native analyzer packaged to ${OUTPUT_DIR}"
|
||||
echo " Archive: ${ARCHIVE_NAME}.tar.gz"
|
||||
echo " RIDs: ${RIDS[*]}"
|
||||
25
devops/services/sealed-mode-ci/README.md
Normal file
25
devops/services/sealed-mode-ci/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Sealed-Mode CI Harness
|
||||
|
||||
This harness supports `DEVOPS-AIRGAP-57-002` by exercising services with the `sealed` flag, verifying that no outbound network traffic succeeds, and producing artefacts Authority can use for `AUTH-AIRGAP-57-001` gating.
|
||||
|
||||
## Workflow
|
||||
1. Run `./run-sealed-ci.sh` from this directory (the script now boots the stack, applies the iptables guard, and captures artefacts automatically).
|
||||
2. The harness:
|
||||
- Launches `sealed-mode-compose.yml` with Authority/Signer/Attestor + Mongo.
|
||||
- Snapshots iptables, injects a `STELLAOPS_SEALED` chain into `DOCKER-USER`/`OUTPUT`, and whitelists only loopback + RFC1918 ranges so container egress is denied.
|
||||
- Repeatedly polls `/healthz` on `5088/6088/7088` to verify sealed-mode bindings stay healthy while egress is blocked.
|
||||
- Executes `egress_probe.py`, which runs curl probes from inside the compose network to confirm off-cluster addresses are unreachable.
|
||||
- Writes logs, iptables counters, and the summary contract to `artifacts/sealed-mode-ci/<timestamp>`.
|
||||
3. `.gitea/workflows/build-test-deploy.yml` now includes a `sealed-mode-ci` job that runs this script on every push/PR and uploads the artefacts for `AUTH-AIRGAP-57-001`.
|
||||
|
||||
## Outputs
|
||||
- `authority.health.log`, `signer.health.log`, `attestor.health.log`
|
||||
- `iptables-docker-user.txt`, `iptables-output.txt`
|
||||
- `egress-probe.json`
|
||||
- `compose.log`, `compose.ps`
|
||||
- `authority-sealed-ci.json` (single file Authority uses to validate the run)
|
||||
|
||||
## TODO
|
||||
- [ ] Wire into offline kit smoke tests (DEVOPS-AIRGAP-58-001).
|
||||
|
||||
Refer to `docs/security/dpop-mtls-rollout.md` for cross-guild milestones.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user