Skip to content

Preview Infrastructure: dataSource Restructure - Implementation Specification

Status: Completed (Phase 1) Author: Claude (Senior Systems Architect) Date: 2026-02-11 Updated: 2026-02-12 Target: Preview Infrastructure Helm chart, pr-preview workflow, cluster-gitops configs


Executive Summary

All 5 active preview environments are stuck because the database-lifecycle.yaml template renders an invalid DatabaseLifecycle CR when snapshotRestore.enabled: false (the default). The template falls through to seeding.enabled: true with type: custom but provides no sourceDatabase or collections, causing the controller to write db-ready ConfigMap with status: failed. Services' init containers then poll forever waiting for status: complete.

The root cause is three independent config trees (snapshotRestore.enabled, databaseLifecycle.seeding.type, mongodb.collections) that must be kept in sync manually. This plan replaces them with a single databaseLifecycle.dataSource field supporting three modes:

Mode Behaviour When to use
"empty" No seeding. Writes db-ready immediately. Default for all PRs (fast startup)
"mock" Runs a mock-data seeding job from fixtures Testing with fabricated data (Phase 2)
"snapshot" Full production snapshot restore + index rebuild Realistic data for performance/integration testing

Phase 1 (this PR) restructures values, fixes the template, updates the workflow, and migrates cluster-gitops configs to unblock all current previews. Phase 2 (future) implements the mock data scaffolding. Phase 3 (future) adds a PM-service-native seed mode.


Table of Contents

  1. High-Level Architecture
  2. Detailed Design
  3. Execution Flow
  4. Edge Cases & Mitigations
  5. Testing Strategy
  6. Implementation Checklist
  7. Open Questions
  8. References

1. High-Level Architecture

1.1 Overview

The change is localised to four files across two repositories:

syrf monorepo (PR branch)
├── src/charts/preview-infrastructure/
│   ├── values.yaml                          # Restructured defaults
│   └── templates/
│       └── database-lifecycle.yaml          # New conditional logic + validation
└── .github/workflows/pr-preview.yml         # Updated write-versions heredoc

cluster-gitops (direct push to HEAD)
└── syrf/environments/preview/
    └── pr-*/infrastructure.values.yaml      # Migrated to new schema (5 files)

The ApplicationSet (syrf-previews.yaml) requires no changes — it passes values through to the Helm chart without interpreting them.

1.2 Current vs Proposed Config Model

Current (three independent trees, must be kept in sync):

# values.yaml (chart defaults)
snapshotRestore:
  enabled: false                    # ← controls snapshot path
  sourceDatabase: "syrf_snapshot"   # ← only used when enabled
mongodb:
  collections: [pmProject, ...]     # ← only used when snapshot enabled
databaseLifecycle:
  seeding:
    enabled: true                   # ← always true by default (BUG)
    type: "snapshot"                # ← defaults to snapshot even when disabled

Proposed (single source of truth):

# values.yaml (chart defaults)
databaseLifecycle:
  dataSource: "empty"               # "empty" | "mock" | "snapshot"
  snapshot:                          # Only read when dataSource=snapshot
    sourceDatabase: "syrf_snapshot"
    collections: [pmProject, ...]
    postRestore:
      indexInit: true
  mock:                              # Only read when dataSource=mock (Phase 2)
    timeout: 300

1.3 Dependencies

  • DatabaseLifecycle controller: Processes the CR and creates the db-ready ConfigMap. Lives outside this repo — no changes needed as long as the CR spec remains valid.
  • Service init containers: Poll db-ready ConfigMap. No changes needed — they already handle status: complete and status: failed.
  • github-notifier-job: PostSync hook that polls db-ready ConfigMap. No changes needed — it already handles all statuses.
  • ArgoCD ApplicationSet: Passes values through. No changes needed.

1.4 Integration Points

pr-preview.yml workflow
    │ writes infrastructure.values.yaml
cluster-gitops/syrf/environments/preview/pr-{N}/infrastructure.values.yaml
    │ read by ArgoCD ApplicationSet git generator
ArgoCD renders preview-infrastructure Helm chart
    │ using values from infrastructure.values.yaml + chart defaults
database-lifecycle.yaml template → DatabaseLifecycle CR
    │ watched by controller
db-ready ConfigMap (status: complete | failed | seeding)
    │ polled by
    ├── service init containers (wait-for-database)
    └── github-notifier-job (PostSync)

2. Detailed Design

2.1 New values.yaml Structure

Replace the current split config with:

# === DATABASE LIFECYCLE ===
databaseLifecycle:
  # Data source mode for this preview environment
  # - "empty":    No seeding. Empty database. db-ready written immediately. (Default)
  # - "mock":     Fabricated test data from fixtures. (Phase 2 - not yet implemented)
  # - "snapshot": Full production snapshot restore + index rebuild. (Requires use-snapshot label)
  dataSource: "empty"

  # What to do if database already exists:
  # - "drop" = clean slate (default for rebuilds)
  # - "skip" = preserve data (lock-db mode)
  # - "fail" = error if database exists
  existingDatabasePolicy: "drop"

  # Seed tracking for init container coordination
  seedId: ""       # GUID that triggers reseed detection
  seedSha: ""      # Commit SHA for audit trail only

  # Lock database (preservation mode)
  lockDatabase: false

  # Snapshot-specific configuration (only used when dataSource=snapshot)
  snapshot:
    sourceDatabase: "syrf_snapshot"
    collections:
      - pmProject
      - pmStudy
      - pmInvestigator
      - pmSystematicSearch
      - pmDataExportJob
      - pmStudyCorrection
      - pmInvestigatorUsage
      - pmRiskOfBiasAiJob
      - pmProjectDailyStat
      - pmPotential
      - pmInvestigatorEmail
    postRestore:
      indexInit: true      # Run index-init job after restore
      timeout: 2400        # 40 minutes for index creation

  # Mock-specific configuration (only used when dataSource=mock, Phase 2)
  mock:
    timeout: 300           # 5 minutes for mock data generation

  # Timeouts for operations
  timeouts:
    seed: 7200         # 2 hours for snapshot restore
    cleanup: 300       # 5 minutes for cleanup
    healthCheck: 60    # 1 minute for health check

Removed fields (consolidated into above): - snapshotRestore.enabled → replaced by dataSource: "snapshot" - snapshotRestore.sourceDatabase → moved to databaseLifecycle.snapshot.sourceDatabase - mongodb.collections → moved to databaseLifecycle.snapshot.collections - databaseLifecycle.seeding.enabled → derived from dataSource != "empty" - databaseLifecycle.seeding.type → derived from dataSource - databaseLifecycle.postSeedJob.enabled → moved to databaseLifecycle.snapshot.postRestore.indexInit - indexInit.enabled → consolidated into snapshot.postRestore.indexInit

Preserved fields (unchanged): - mongodb.enabled — still controls whether MongoDB resources are created at all - mongodb.atlasProjectId, mongodb.username, mongodb.database — still needed for AtlasDatabaseUser CR - mongoAtlas.clusterName, mongoAtlas.clusterAddress — still needed for connection config - syncWaves.* — unchanged - images.* — unchanged - deploymentNotification.* — unchanged

2.2 Updated database-lifecycle.yaml Template

{{- if .Values.mongodb.enabled }}
{{/* === Validation: fail fast on invalid combinations === */}}
{{- $dataSource := .Values.databaseLifecycle.dataSource | default "empty" }}
{{- if not (has $dataSource (list "empty" "mock" "snapshot")) }}
  {{- fail (printf "Invalid databaseLifecycle.dataSource: %q. Must be one of: empty, mock, snapshot" $dataSource) }}
{{- end }}
{{- if and (eq $dataSource "snapshot") (not .Values.databaseLifecycle.snapshot.sourceDatabase) }}
  {{- fail "databaseLifecycle.dataSource is 'snapshot' but snapshot.sourceDatabase is not set" }}
{{- end }}
{{- if and (eq $dataSource "snapshot") (not .Values.databaseLifecycle.snapshot.collections) }}
  {{- fail "databaseLifecycle.dataSource is 'snapshot' but snapshot.collections is empty" }}
{{- end }}

apiVersion: database.syrf.org.uk/v1alpha1
kind: DatabaseLifecycle
metadata:
  name: pr-database
  namespace: {{ include "preview-infrastructure.namespace" . }}
  labels:
    {{- include "preview-infrastructure.labels" . | nindent 4 }}
    app.kubernetes.io/component: database-lifecycle
  annotations:
    argocd.argoproj.io/sync-wave: {{ .Values.syncWaves.databaseLifecycle | quote }}
spec:
  database: {{ .Values.mongodb.database }}

  {{- if .Values.databaseLifecycle.seedId }}
  seedId: {{ .Values.databaseLifecycle.seedId | quote }}
  {{- end }}
  {{- if .Values.databaseLifecycle.seedSha }}
  seedSha: {{ .Values.databaseLifecycle.seedSha | quote }}
  {{- end }}

  lockDatabase: {{ .Values.databaseLifecycle.lockDatabase | default false }}

  {{- if eq $dataSource "empty" }}
  # Empty mode: no seeding, db-ready written immediately
  seeding:
    enabled: false
  {{- else if eq $dataSource "mock" }}
  # Mock mode: fabricated test data (Phase 2)
  seeding:
    enabled: true
    type: custom
    timeout: {{ .Values.databaseLifecycle.mock.timeout | default 300 }}
    killOnDelete: true
  {{- else if eq $dataSource "snapshot" }}
  # Snapshot mode: full production restore
  seeding:
    enabled: true
    type: snapshot
    sourceDatabase: {{ .Values.databaseLifecycle.snapshot.sourceDatabase }}
    collections:
      {{- range .Values.databaseLifecycle.snapshot.collections }}
      - {{ . }}
      {{- end }}
    timeout: {{ .Values.databaseLifecycle.timeouts.seed | default 7200 }}
    killOnDelete: true
  {{- end }}

  existingDatabasePolicy: {{ .Values.databaseLifecycle.existingDatabasePolicy | default "drop" }}
  cleanupOnDelete: true

  {{- if eq $dataSource "snapshot" }}
  # Wait for service deployments to scale down before seeding
  watchedDeployments:
    labelSelector: "syrf.org.uk/uses-database=true"
    timeout: {{ .Values.databaseLifecycle.watchedDeploymentsTimeout | default 300 }}
  {{- end }}

  connection:
    secretRef:
      name: syrf-{{ .Values.mongoAtlas.clusterName }}-{{ .Values.mongodb.username | replace "_" "-" }}
      key: connectionStringPrivateSrv

  timeouts:
    seed: {{ .Values.databaseLifecycle.timeouts.seed | default 7200 }}
    cleanup: {{ .Values.databaseLifecycle.timeouts.cleanup | default 300 }}
    healthCheck: {{ .Values.databaseLifecycle.timeouts.healthCheck | default 60 }}

  {{/* Post-seed job: only for snapshot mode with indexInit enabled */}}
  {{- $indexInitEnabled := not (eq (dig "snapshot" "postRestore" "indexInit" true .Values.databaseLifecycle) false) }}
  {{- if and (eq $dataSource "snapshot") $indexInitEnabled }}
  postSeedJob:
    enabled: true
    timeout: {{ dig "snapshot" "postRestore" "timeout" 2400 .Values.databaseLifecycle }}
    killOnDelete: true
    jobTemplate:
      inline:
        spec:
          template:
            metadata:
              labels:
                app.kubernetes.io/name: index-init
                app.kubernetes.io/component: database-lifecycle
            spec:
              containers:
              - name: index-init
                image: "{{ .Values.images.projectManagement.repository }}:sha-{{ .Values.shortSha }}"
                imagePullPolicy: Always
                env:
                - name: SYRF_INDEX_INIT_MODE
                  value: "true"
                - name: VERSION
                  value: {{ .Values.gitVersion.version | default "" | quote }}
                - name: SYRF__GitVersion__SemVer
                  value: {{ .Values.gitVersion.version | default "" | quote }}
                - name: SYRF__GitVersion__FullSemVer
                  value: {{ .Values.gitVersion.fullSemVer | default "" | quote }}
                - name: SYRF__GitVersion__InformationalVersion
                  value: {{ .Values.gitVersion.informationalVersion | default "" | quote }}
                - name: SYRF__GitVersion__Sha
                  value: {{ .Values.gitVersion.sha | default "" | quote }}
                - name: SYRF__AppSettingsConfig__BuildNumber
                  value: {{ .Values.gitVersion.version | default "" | quote }}
                - name: SYRF__AppSettingsConfig__DefaultSchemaVersion
                  value: "0"
                - name: SYRF__AppSettingsConfig__UiUrl
                  value: "https://pr-{{ .Values.prNumber }}.syrf.org.uk"
                - name: SYRF__AppSettingsConfig__MinUiVersion
                  value: "0.0.0"
                - name: SYRF__RuntimeEnvironment
                  value: {{ .Values.environment.name | default "preview" | quote }}
                - name: SYRF__ActiveEnvironment
                  value: {{ .Values.environment.name | default "preview" | quote }}
                - name: SYRF__ConnectionStrings__MongoConnection__Username
                  valueFrom:
                    secretKeyRef:
                      name: syrf-{{ .Values.mongoAtlas.clusterName }}-{{ .Values.mongodb.username | replace "_" "-" }}
                      key: username
                - name: SYRF__ConnectionStrings__MongoConnection__Password
                  valueFrom:
                    secretKeyRef:
                      name: syrf-{{ .Values.mongoAtlas.clusterName }}-{{ .Values.mongodb.username | replace "_" "-" }}
                      key: password
                - name: SYRF__ConnectionStrings__MongoConnection__ClusterAddress
                  value: {{ .Values.mongoAtlas.clusterAddress | quote }}
                - name: SYRF__ConnectionStrings__MongoConnection__DatabaseName
                  value: {{ .Values.mongodb.database | quote }}
                - name: SYRF__ConnectionStrings__MongoConnection__AuthDb
                  value: "admin"
                - name: SYRF__ConnectionStrings__MongoConnection__UseSSL
                  value: "true"
                resources:
                  requests:
                    memory: "256Mi"
                    cpu: "100m"
                  limits:
                    memory: "512Mi"
                    cpu: "500m"
              restartPolicy: Never
              imagePullSecrets:
              - name: ghcr-secret
          backoffLimit: 3
          ttlSecondsAfterFinished: 3600
  {{- end }}

  scripts:
    healthCheckScript:
      type: mongosh
      content: |
        const targetDb = db.getSiblingDB('{{ .Values.mongodb.database }}');
        const meta = targetDb.snapshot_metadata.findOne({_id: 'latest'});
        if (!meta) {
          print('Metadata not yet created - seeding in progress');
        } else {
          print('Database status: ' + meta.status);
          print('Collections: ' + (meta.collections || []).length);
        }
{{- end }}

2.3 Updated pr-preview.yml write-versions Job

Replace the current infrastructure.values.yaml heredoc with:

# Determine dataSource mode
if [ "${USE_SNAPSHOT}" = "true" ] && [ "${LOCK_DB}" != "true" ]; then
  DATA_SOURCE="snapshot"
else
  DATA_SOURCE="empty"
fi

cat > "${PR_DIR}/infrastructure.values.yaml" <<EOF
# Auto-generated by pr-preview workflow
# Values for preview-infrastructure Helm chart

# PR identification
prNumber: "${PR_NUM}"
headSha: "${HEAD_SHA}"
shortSha: "${HEAD_SHORT_SHA}"
branch: "${BRANCH}"

# Namespace configuration
namespace:
  enabled: true

# MongoDB Atlas configuration
mongodb:
  enabled: ${MONGODB_ENABLED}
  atlasProjectId: "${ATLAS_PROJECT_ID}"
  username: "syrf_pr_${PR_NUM}_app"
  database: "syrf_pr_${PR_NUM}"

# MongoDB Atlas cluster configuration (Preview Cluster)
mongoAtlas:
  clusterName: "${PREVIEW_CLUSTER_NAME}"
  clusterAddress: "${PREVIEW_CLUSTER_HOST}"

# DatabaseLifecycle configuration
databaseLifecycle:
  dataSource: "${DATA_SOURCE}"
  existingDatabasePolicy: "${EXISTING_POLICY}"
  seedId: "${SEED_ID}"
  seedSha: "${SEED_SHA}"
  lockDatabase: ${LOCK_DATABASE}
EOF

Changes: - Removed snapshotRestore: block entirely - Replaced with databaseLifecycle.dataSource set to "empty" or "snapshot" - Snapshot-specific values (sourceDatabase, collections) are in chart defaults — only overridden when dataSource=snapshot

2.4 Cluster-GitOps Migration

For each existing pr-*/infrastructure.values.yaml, remove:

# REMOVE:
snapshotRestore:
  enabled: false

Add:

# ADD (under databaseLifecycle):
  dataSource: "empty"

All 5 active PR configs (2126, 2230, 2300, 2301, 2322) currently have snapshotRestore.enabled: false, so they all migrate to dataSource: "empty".

2.5 Scale-Down Templates

The existing scale-down-rbac.yaml and scale-down-job.yaml templates currently gate on snapshotRestore.enabled. Update their conditions:

# Before:
{{- if .Values.snapshotRestore.enabled }}

# After:
{{- if eq (.Values.databaseLifecycle.dataSource | default "empty") "snapshot" }}

2.6 ApplicationSet Parameters

The ApplicationSet passes seedId and seedSha to the infrastructure app via parameters. These are unchanged — they're already under databaseLifecycle.* in the values and don't reference snapshotRestore. No ApplicationSet changes needed.


3. Execution Flow

3.1 Happy Path: dataSource: "empty" (Default)

1. pr-preview.yml writes infrastructure.values.yaml with dataSource: "empty"
2. ArgoCD detects change, renders preview-infrastructure chart
3. database-lifecycle.yaml renders DatabaseLifecycle CR with:
     seeding:
       enabled: false
4. Controller sees seeding.enabled=false → writes db-ready ConfigMap:
     status: complete
5. Service init containers see status=complete → pods start
6. github-notifier-job sees db-ready=complete → checks service health → notifies GitHub
7. Preview is live within minutes (no seeding delay)

3.2 Happy Path: dataSource: "snapshot"

1. PR has `use-snapshot` label → workflow writes dataSource: "snapshot"
2. ArgoCD renders DatabaseLifecycle CR with:
     seeding:
       enabled: true
       type: snapshot
       sourceDatabase: syrf_snapshot
       collections: [pmProject, pmStudy, ...]
3. Controller seeds database from snapshot (~52 min for pmStudy)
4. Post-seed job runs index-init (~22 min for 31 indexes)
5. Controller writes db-ready ConfigMap: status: complete
6. Service init containers unblock → pods start
7. github-notifier-job notifies GitHub

3.3 Validation Failure Path

1. Someone manually sets dataSource: "snapshot" without sourceDatabase
2. Helm template `fail` function fires during ArgoCD render
3. ArgoCD Application shows SyncFailed with clear error message:
     "databaseLifecycle.dataSource is 'snapshot' but snapshot.sourceDatabase is not set"
4. No invalid CR is ever created
5. Fix: correct the values and ArgoCD auto-retries

3.4 Lock-DB Path (Unchanged)

1. PR has `lock-db` label → workflow writes:
     lockDatabase: true
     existingDatabasePolicy: "skip"
     dataSource: "empty"     # Lock-db never seeds
2. Controller sees lockDatabase=true → preserves existing data
3. db-ready ConfigMap written with status: complete

4. Edge Cases & Mitigations

# Edge Case / Failure Mode Impact Mitigation
1 Mixed old/new values: ArgoCD syncs before cluster-gitops configs are updated Medium - old snapshotRestore.enabled field is ignored, chart uses dataSource default of "empty" Safe: old fields become no-ops. The chart no longer reads snapshotRestore.*. Default dataSource: "empty" unblocks the preview.
2 Stale db-ready ConfigMap: Previous failed ConfigMap exists when switching to empty mode Low - controller should overwrite Controller overwrites ConfigMap on CR reconciliation. If not, delete the ConfigMap manually and ArgoCD will re-sync.
3 Snapshot without label: User forgets use-snapshot label, expects data Low - preview starts with empty DB This is the correct default behaviour. Add label and re-sync to trigger snapshot.
4 Mid-seeding mode switch: dataSource changes from snapshot to empty while seeding is in progress Medium - seeding job may be orphaned DatabaseLifecycle CR has killOnDelete: true — the controller kills the seed job when the CR spec changes.
5 dataSource: "mock" before Phase 2: Someone sets mock mode before scaffolding exists Medium - seeding job template missing, CR invalid Helm validation: add fail if dataSource=mock and mock job template is not defined. For Phase 1, emit a clear error.
6 Legacy pr-2224/pr-2271 directories: Old format with standalone manifests None - these are already handled by the workflow's legacy cleanup code (lines 1446-1454) No action needed.
7 Chart version drift: cluster-gitops values reference fields that don't exist in chart None - Helm ignores unknown values Safe: snapshotRestore.enabled: false in old configs is simply ignored.
8 Concurrent PR workflows: Two workflow runs write the same infrastructure.values.yaml Low - last write wins Already the case. The workflow uses commit-and-push with retry logic.

5. Testing Strategy

5.1 Helm Template Validation

Test each mode renders correctly:

# Empty mode (default)
helm template test src/charts/preview-infrastructure/ \
  --set prNumber=9999 \
  --set headSha=abc1234 \
  --set shortSha=abc1234 \
  --set mongodb.enabled=true \
  --set mongodb.database=syrf_pr_9999 \
  --set mongodb.username=syrf_pr_9999_app \
  | grep -A10 "kind: DatabaseLifecycle"
# Expect: seeding.enabled: false

# Snapshot mode
helm template test src/charts/preview-infrastructure/ \
  --set prNumber=9999 \
  --set headSha=abc1234 \
  --set shortSha=abc1234 \
  --set mongodb.enabled=true \
  --set mongodb.database=syrf_pr_9999 \
  --set mongodb.username=syrf_pr_9999_app \
  --set databaseLifecycle.dataSource=snapshot \
  | grep -A20 "kind: DatabaseLifecycle"
# Expect: seeding.enabled: true, type: snapshot, sourceDatabase: syrf_snapshot

# Invalid mode (should fail)
helm template test src/charts/preview-infrastructure/ \
  --set prNumber=9999 \
  --set headSha=abc1234 \
  --set shortSha=abc1234 \
  --set mongodb.enabled=true \
  --set mongodb.database=syrf_pr_9999 \
  --set mongodb.username=syrf_pr_9999_app \
  --set databaseLifecycle.dataSource=invalid \
  2>&1 | grep "Invalid databaseLifecycle.dataSource"
# Expect: error message

# Snapshot without sourceDatabase (should fail)
helm template test src/charts/preview-infrastructure/ \
  --set prNumber=9999 \
  --set headSha=abc1234 \
  --set shortSha=abc1234 \
  --set mongodb.enabled=true \
  --set mongodb.database=syrf_pr_9999 \
  --set mongodb.username=syrf_pr_9999_app \
  --set databaseLifecycle.dataSource=snapshot \
  --set databaseLifecycle.snapshot.sourceDatabase="" \
  2>&1 | grep "sourceDatabase is not set"
# Expect: error message

5.2 End-to-End Verification

After deploying:

  1. Check ArgoCD shows all 5 infrastructure apps as Healthy/Synced
  2. Verify db-ready ConfigMaps have status: complete in all PR namespaces:
    for ns in pr-2126 pr-2230 pr-2300 pr-2301 pr-2322; do
      kubectl get configmap db-ready -n $ns -o jsonpath='{.data.status}' 2>/dev/null
      echo " ($ns)"
    done
    
  3. Verify service pods are running (not stuck in Init:0/1):
    for ns in pr-2126 pr-2230 pr-2300 pr-2301 pr-2322; do
      echo "=== $ns ==="
      kubectl get pods -n $ns --no-headers | head -5
    done
    
  4. Verify github-notifier-job completes and posts deployment status

6. Implementation Checklist

Phase 1: Restructure + Unblock (Single PR) — COMPLETED

  • 1.1 Update src/charts/preview-infrastructure/values.yaml
  • Add databaseLifecycle.dataSource: "empty" default
  • Move snapshotRestore.sourceDatabasedatabaseLifecycle.snapshot.sourceDatabase
  • Move mongodb.collectionsdatabaseLifecycle.snapshot.collections
  • Add databaseLifecycle.snapshot.postRestore section
  • Add databaseLifecycle.mock section (placeholder for Phase 2)
  • Remove snapshotRestore section
  • Remove indexInit section (consolidated)
  • Remove databaseLifecycle.seeding section
  • Remove databaseLifecycle.postSeedJob section
  • Bump chart version to 0.3.0

  • 1.2 Update src/charts/preview-infrastructure/templates/database-lifecycle.yaml

  • Add Helm validation block (fail on invalid dataSource, missing snapshot config)
  • Replace three-way if/else if/else with dataSource-based branching
  • Gate watchedDeployments on dataSource=snapshot instead of snapshotRestore.enabled
  • Gate postSeedJob on dataSource=snapshot + snapshot.postRestore.indexInit
  • Add dataSource=mock validation (fail until implemented)
  • Implementation note: Empty mode skips the DatabaseLifecycle CR entirely; db-ready-configmap.yaml creates the ConfigMap directly instead

  • 1.3 Update MongoDB Atlas user template

  • Changed condition from snapshotRestore.enabled to eq dataSource "snapshot" for read-only role assignment
  • Scale-down templates were not present (controller handles orchestration)

  • 1.4 Update .github/workflows/pr-preview.yml write-versions job

  • Replace SNAPSHOT_RESTORE_ENABLED variable with DATA_SOURCE
  • Remove snapshotRestore: section from heredoc
  • Add databaseLifecycle.dataSource: to heredoc
  • Keep databaseLifecycle.existingDatabasePolicy, seedId, seedSha, lockDatabase as-is

  • 1.5 Run Helm template validation — 105 tests passing (5 test suites)

  • 1.6 Commit and push to PR branch

  • 1.7 Update cluster-gitops configs (post-merge)

  • Existing configs with snapshotRestore.enabled: false are safe — the field is ignored and dataSource defaults to "empty". Migration deferred to next workflow run per PR.

  • 1.8 Verify ArgoCD syncs and previews unblock (section 5.2)

Phase 2: Mock Data Scaffolding (Future PR)

  • 2.1 Design mock data fixture format (JSON/BSON files in chart or separate image)
  • 2.2 Add mock seeding job template to database-lifecycle.yaml
  • 2.3 Add databaseLifecycle.mock.image to values
  • 2.4 Remove Helm fail guard for dataSource=mock
  • 2.5 Update workflow to support use-mock-data label

Phase 3: PM Service Native Seed Mode (Future)

  • 3.1 Add SYRF_SEED_MODE environment variable to PM service
  • 3.2 PM service generates minimal valid data on startup when SYRF_SEED_MODE=true
  • 3.3 Wire into dataSource=mock job template

7. Open Questions

  1. Mock data Phase 2 timing: Is mock data needed before the snapshot pipeline is fully operational, or is empty sufficient for now?

  2. Legacy PR cleanup: Should we delete the stale pr-2224 and pr-2271 directories from cluster-gitops as part of this work, or leave them for the workflow's existing cleanup code?

  3. Chart version bump: 0.3.0 seems appropriate for a breaking values schema change. Confirm?


8. References

  • preview-infrastructure chart: src/charts/preview-infrastructure/
  • pr-preview.yml: .github/workflows/pr-preview.yml
  • syrf-previews.yaml: cluster-gitops/argocd/applicationsets/syrf-previews.yaml
  • database-lifecycle.yaml template: src/charts/preview-infrastructure/templates/database-lifecycle.yaml

Document End

This document must be reviewed and approved before implementation begins.