diff --git a/.github/agents/bc-gov-standards.agent.md b/.claude/agents/bc-gov-standards.md similarity index 73% rename from .github/agents/bc-gov-standards.agent.md rename to .claude/agents/bc-gov-standards.md index a1654b8..3e735ca 100644 --- a/.github/agents/bc-gov-standards.agent.md +++ b/.claude/agents/bc-gov-standards.md @@ -1,21 +1,24 @@ -# BC Gov Standards Agent -# Agent Skill: bc-gov-standards -# Ryan Loisell — Developer / Architect -# GitHub Copilot — AI pair programmer / code generation -# February 2026 -# -# This agent skill provides awareness of all BC Government DevOps, Design, -# Security, and Deployment standards relevant to new projects in the bcgov-c -# organization, deployed to OpenShift Emerald. -# -# Self-learning: append new standards discoveries to STANDARDS_KNOWLEDGE below. - -## Identity - -You are the **BC Gov Standards Advisor** for HelloNetworkWorld. -Your role is to ensure all code, configuration, and documentation conforms to -current BC Government standards. When asked about any standard, reference the -authoritative source and flag any known gaps. +--- +name: bc-gov-standards +description: Enforces BC Government DevOps, Design System, security, and deployment standards for HelloNetworkWorld on Emerald be808f. Covers DataClass labelling, Artifactory image registry, Common SSO (Keycloak), NetworkPolicy rules, port standards, and secret handling. References authoritative BC Gov sources. +tools: + - Read + - Grep + - Glob +model: sonnet +permissionMode: default +memory: project +--- + +# BC Gov Standards Agent — HelloNetworkWorld + +**Ryan Loiselle** — Developer / Architect +**GitHub Copilot** — AI pair programmer / code generation +**February 2026** + +This agent ensures all code, configuration, and documentation conforms to +current BC Government standards for projects in the bcgov-c organization deployed +to OpenShift Emerald. ## Scope @@ -30,12 +33,12 @@ authoritative source and flag any known gaps. ## Core Rules 1. **DataClass**: All pods in be808f require `DataClass: Medium` label. Routes require `aviinfrasetting.ako.vmware.com/name: dataclass-medium` annotation. NEVER use `dataclass-low` — it has no VIP on Emerald. -2. **Design System**: All UI components must use BC Gov Design System tokens. Import from `@bcgov/design-tokens`. Never use hardcoded hex colours — reference the palette constants in `DashboardPage.jsx`. +2. **Design System**: All UI components must use BC Gov Design System tokens. Import from `@bcgov/design-tokens`. Never use hardcoded hex colours. 3. **Auth**: Phase 1 = public. Phase 2 = Keycloak OIDC via `common-sso.justice.gov.bc.ca`, realm `standard`. Never implement custom auth. 4. **Images**: Only push to `artifacts.developer.gov.bc.ca/dbe8-docker-local/`. Never use Docker Hub images in production. 5. **Ports**: Always use 8080 in containers. Never expose 80, 443, or 5000 in OpenShift. 6. **NetworkPolicy**: Default-deny ingress and egress. Explicitly allow each required flow. Rules are always two-way. -7. **Secrets**: Never commit secrets. Use OpenShift Secrets mounted as environment variables. Reference `SECRETS_README.md` pattern from DSC-modernization. +7. **Secrets**: Never commit secrets. Use OpenShift Secrets mounted as environment variables. ## Authoritative Sources diff --git a/.github/agents/network-policy.agent.md b/.claude/agents/network-policy.md similarity index 81% rename from .github/agents/network-policy.agent.md rename to .claude/agents/network-policy.md index 2da9264..e5a1a3b 100644 --- a/.github/agents/network-policy.agent.md +++ b/.claude/agents/network-policy.md @@ -1,20 +1,25 @@ -# Network Policy Agent -# Agent Skill: network-policy -# Ryan Loiselle — Developer / Architect -# GitHub Copilot — AI pair programmer / code generation -# February 2026 -# -# This agent skill generates and validates OpenShift NetworkPolicy YAML for -# the HelloNetworkWorld project on Emerald be808f. -# Rules: default-deny ingress+egress, two-way explicit allowance, DataClass Medium. -# -# Self-learning: append new policy variations to PATTERNS_KNOWLEDGE below. - -## Identity - -You are the **Network Policy Advisor** for HelloNetworkWorld. -You generate, validate, and explain Kubernetes/OpenShift NetworkPolicy YAML -conforming to Emerald be808f standards. +--- +name: network-policy +description: Generates and validates OpenShift NetworkPolicy YAML for HelloNetworkWorld on Emerald be808f. Enforces default-deny ingress+egress, two-way explicit allowance, DataClass Medium labelling, and AVI InfraSettings annotation (dataclass-medium). Automates NetworkPolicy generation for Feature 007 NetworkTestDefinition flow. +tools: + - Read + - Write + - Grep + - Glob + - Bash +model: sonnet +permissionMode: default +memory: project +--- + +# Network Policy Agent — HelloNetworkWorld + +**Ryan Loiselle** — Developer / Architect +**GitHub Copilot** — AI pair programmer / code generation +**February 2026** + +This agent generates, validates, and explains Kubernetes/OpenShift NetworkPolicy YAML +conforming to Emerald be808f standards for the HelloNetworkWorld project. ## Core Rules @@ -132,10 +137,7 @@ spec: ``` > **Note**: `DatabaseServer` tests connectivity to **external databases outside the namespace**. -> Projects may require connectivity to existing data stores (Oracle on-prem, SQL Server, -> external PostgreSQL, etc.). The `DatabaseServer` service type is NOT for the app's own -> MariaDB (that is covered by the API→DB in-namespace policy). Feature 007 should generate -> per-destination NetworkPolicy rules with the specific host CIDR and port. +> Feature 007 should generate per-destination NetworkPolicy rules with the specific host CIDR and port. ## Network Policy Automation (Feature 007) diff --git a/.github/agents/openshift-health.agent.md b/.claude/agents/openshift-health.md similarity index 79% rename from .github/agents/openshift-health.agent.md rename to .claude/agents/openshift-health.md index 5708ee3..5d1d976 100644 --- a/.github/agents/openshift-health.agent.md +++ b/.claude/agents/openshift-health.md @@ -1,16 +1,23 @@ -# OpenShift Health Agent -# Agent Skill: openshift-health -# Ryan Loiselle — Developer / Architect -# GitHub Copilot — AI pair programmer / code generation -# February 2026 -# -# This agent skill helps inspect and troubleshoot OpenShift resources in be808f, -# and guides correct health check endpoint implementation patterns. - -## Identity - -You are the **OpenShift Health Advisor** for HelloNetworkWorld. -You assist with OCP resource inspection, health check patterns, and deployment +--- +name: openshift-health +description: Inspects and troubleshoots OpenShift resources in the HelloNetworkWorld be808f namespace. Guides correct health check endpoint implementation for ASP.NET Core (.NET 10) API and Nginx frontend. Provides oc and argocd commands for the be808f-dev/test/prod/tools namespaces. +tools: + - Read + - Bash + - Grep + - Glob +model: haiku +permissionMode: default +memory: project +--- + +# OpenShift Health Agent — HelloNetworkWorld + +**Ryan Loiselle** — Developer / Architect +**GitHub Copilot** — AI pair programmer / code generation +**February 2026** + +This agent assists with OCP resource inspection, health check patterns, and deployment troubleshooting in the Emerald be808f namespace. ## Namespace Reference diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..bc7b48b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1 @@ +{"plugins":[".github/agents"]} diff --git a/.github/agents b/.github/agents new file mode 160000 index 0000000..e7dddcd --- /dev/null +++ b/.github/agents @@ -0,0 +1 @@ +Subproject commit e7dddcde66058f388e523359fc5d0b393be0be07 diff --git a/.github/agents/README.md b/.github/agents/README.md deleted file mode 100644 index 433c9ed..0000000 --- a/.github/agents/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# GitHub Copilot Agent Skills — rl-project-template - -**Author**: Ryan Loiselle — Developer / Architect -**AI tool**: GitHub Copilot — AI pair programmer / code generation -**Updated**: February 2026 - -This folder contains GitHub Copilot **agent skills** following the -[Agent Skills open standard](https://agentskills.io). Each skill is a directory -containing a `SKILL.md` file with YAML frontmatter. VS Code and GitHub Copilot -discover all skills in `.github/agents/` automatically. - -Skills are **progressively disclosed**: -- **Startup** — only `name` + `description` loaded (~100 tokens per skill) -- **On activation** — full `SKILL.md` loaded when a task matches the skill -- **On demand** — `references/` files loaded only when specifically needed - ---- - -## Agent Team (9 skills) - -| Directory | Scope | -|-----------|-------| -| `session-workflow/` | Session startup/shutdown, AI file maintenance (WORKLOG/CHANGES/COMMANDS/COMMIT_INFO) | -| `github-workflow/` | Branch naming, PR lifecycle, CI diagnosis, gh CLI, rulesets | -| `diagram-generation/` | draw.io + PlantUML + Mermaid, 10-diagram UML suite, folder structure, export | -| `ci-cd-pipeline/` | 5-workflow pattern, ISB EA Option 2, image tags, yq GitOps, Trivy, failure triage | -| `local-dev/` | podman-compose, EF Core migration commands, MariaDB, port conventions, troubleshooting | -| `spec-kitty/` | Spec-first development, WP YAML format, spec.md/plan.md sections, validate-tasks | -| `ef-core/` | Pomelo/MariaDB patterns, migration workflow, startup Migrate(), Linux LINQ gotcha, service layer | -| `bc-gov-devops/` | Emerald OpenShift, Artifactory, Helm, health checks, Common SSO, oc commands, ArgoCD | -| `agent-evolution/` | Self-learning — monitors sessions, updates KNOWLEDGE sections, promotes shared skills | - ---- - -## Shared Skills (4 skills) - -Reusable skills referenced by multiple agents. VS Code discovers all directories automatically. - -| Directory | Consumed By | -|-----------|-------------| -| `ai-session-files/` | session-workflow, github-workflow | -| `git-conventions/` | session-workflow, github-workflow, ci-cd-pipeline | -| `bc-gov-emerald/` | bc-gov-devops, ci-cd-pipeline | -| `containerfile-standards/` | bc-gov-devops, ci-cd-pipeline | - ---- - -## Directory Structure - -``` -.github/agents/ - ├── session-workflow/ - │ └── SKILL.md - ├── github-workflow/ - │ └── SKILL.md - ├── diagram-generation/ - │ ├── SKILL.md - │ └── references/ - │ └── plantuml-templates.md - ├── ci-cd-pipeline/ - │ └── SKILL.md - ├── local-dev/ - │ └── SKILL.md - ├── spec-kitty/ - │ └── SKILL.md - ├── ef-core/ - │ └── SKILL.md - ├── bc-gov-devops/ - │ ├── SKILL.md - │ └── references/ - │ └── networkpolicy-patterns.md - ├── agent-evolution/ - │ ├── SKILL.md - │ └── references/ - │ └── evolution-log.md - ├── ai-session-files/ ← shared skill - │ └── SKILL.md - ├── git-conventions/ ← shared skill - │ └── SKILL.md - ├── bc-gov-emerald/ ← shared skill - │ └── SKILL.md - └── containerfile-standards/ ← shared skill - └── SKILL.md -``` - ---- - -## SKILL.md Frontmatter Format - -```yaml ---- -name: skill-name # ≤ 64 chars, lowercase/hyphens, must match directory name -description: >- # ≤ 1024 chars, third person, includes what + when to activate - ... -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: -allowed-tools: # optional — restrict which tools the skill may invoke - - read_file ---- -``` - ---- - -## How to Add a New Skill - -1. Create directory: `.github/agents//` -2. Create `SKILL.md` with YAML frontmatter; body < 500 lines recommended -3. If body exceeds 400 lines, split reference content to `references/.md` -4. If ≥ 2 agents share content, extract to a new shared skill directory -5. Update this README's inventory tables -6. Run `agent-evolution` at session end to log the change - -### Referencing Shared Skills - -```markdown -See [`../bc-gov-emerald/SKILL.md`](../bc-gov-emerald/SKILL.md). -``` - ---- - -## Self-Learning - -The `agent-evolution` skill monitors sessions and grows the library: -- Appends discoveries to `*_KNOWLEDGE` sections at session end -- Promotes recurring patterns (≥ 2 agents) to shared skills -- Flags oversized SKILL.md files for `references/` splits -- Tracks all changes in `agent-evolution/references/evolution-log.md` - ---- - -## Related Files - -| File | Purpose | -|------|---------| -| `.github/copilot-instructions.md` | Standing AI instructions (read every session) | -| `AI/nextSteps.md` | MASTER TODO and session history | -| `CODING_STANDARDS.md` | Full coding conventions | diff --git a/.github/agents/agent-evolution/SKILL.md b/.github/agents/agent-evolution/SKILL.md deleted file mode 100644 index 4e17028..0000000 --- a/.github/agents/agent-evolution/SKILL.md +++ /dev/null @@ -1,209 +0,0 @@ ---- -name: agent-evolution -description: Self-learning agent that monitors development sessions and evolves the agent skill library — surfaces past relevant knowledge before sessions begin (pre-session retrieval), appends causal discoveries to *_KNOWLEDGE sections, identifies candidate shared skills from recurring patterns across sessions, flags oversized agents needing references/ splits, and records all updates in the evolution log. Use at session start for retrieval and at session end to grow the team's shared intelligence. -metadata: - author: Ryan Loiselle - version: "1.1" -allowed-tools: - - read_file - - replace_string_in_file - - file_search - - grep_search ---- - -# Agent Evolution Agent - -Grows the agent skill library from lived session experience. - -**Invoke at session end, after COMMIT_INFO is written.** - -The evolution log is at -[`references/evolution-log.md`](references/evolution-log.md). - ---- - -## Responsibilities - -1. **Discovery review** — Read the session's WORKLOG, CHANGES, and COMMANDS files. - Identify reusable patterns, fixes, or new knowledge discovered. - -2. **KNOWLEDGE section updates** — Append discoveries to the `*_KNOWLEDGE` section - of the appropriate SKILL.md. Format: `YYYY-MM-DD: [Project] observation`. - -3. **Shared skill promotion** — If content appears or was applied across 2+ agents - in a single session, evaluate whether it belongs in a shared skill. - -4. **Line count audit** — Flag any SKILL.md file exceeding 400 lines as a candidate - for `references/` split. - -5. **Evolution log** — Record every change made to the skill library. - ---- - -## Session Activation Protocol - -### Step 0 — Surface relevant past knowledge (pre-session) - -Before any implementation work begins, retrieve KNOWLEDGE entries relevant to the current session topic. -This mirrors AgentEvolver's Self-Navigating mechanism — inject past experience *before* acting, not only record it after. - -```bash -# Extract a keyword from the branch name or feature topic -TOPIC="" - -# Find SKILL.md files whose KNOWLEDGE sections mention the topic -grep -ril "$TOPIC" .github/agents/ --include="SKILL.md" - -# Print matching KNOWLEDGE entries from those files -grep -h "^- " .github/agents/*/SKILL.md | grep -i "$TOPIC" -``` - -Surface any matches as context before proceeding. If no matches are found, proceed directly to Step 1. - ---- - -### Step 1 — Read session files - -```bash -# Find the session AI folder -ls AI/ - -# Read today's WORKLOG -cat AI/YYYY-MM-DD-WORKLOG.md - -# Read CHANGES and COMMANDS -cat AI/YYYY-MM-DD-CHANGES.md -cat AI/YYYY-MM-DD-COMMANDS.md -``` - -### Step 2 — Match discoveries to skills - -For each discovery in the session files, find the relevant agent: - -```bash -# Search for relevant topic across all SKILL.md files -grep -r "" .github/agents/ -``` - -### Step 3 — Append to KNOWLEDGE sections - -At the bottom of each relevant SKILL.md, append to the `*_KNOWLEDGE` block. - -**Standard format** (discoveries, patterns): -```markdown -- YYYY-MM-DD: [ProjectName] -``` - -**Causal format** (failures, debugging sessions — preferred for high-signal entries): -```markdown -- YYYY-MM-DD: [ProjectName] — CAUSE: — FIX: -``` - -Include `CAUSE:` / `FIX:` whenever a fix required diagnosis — this attributes the causal step, not just the outcome, making the entry actionable for future sessions. - -Maximum 3 bullet points per session per agent. Keep entries concise (< 150 chars total). - -### Step 4 — Check for shared skill candidates - -Signal to extract to a shared skill when: -- Same pattern written into 2+ separate SKILL.md files this session, OR -- A KNOWLEDGE entry is already present in 2+ agents for the same topic, OR -- A topic keyword appears in 3+ past session entries in `evolution-log.md` without a dedicated shared skill - -```bash -# Scan evolution log for recurring topics not yet promoted to a shared skill -# Replace with the pattern in question -grep -c "" .github/agents/agent-evolution/references/evolution-log.md - -# List all existing shared skills for reference -ls .github/agents/ -``` - -If a cross-session pattern is found (3+ log entries, no shared skill), treat it as a promotion candidate even if it did not re-appear this session. - -Create the shared skill directory and SKILL.md, then replace the inline content -with a reference link. - -### Step 5 — Line count audit - -```bash -# Find SKILL.md files over 400 lines -find .github/agents -name "SKILL.md" | while read f; do - lines=$(wc -l < "$f") - if [ "$lines" -gt 400 ]; then - echo "$lines $f" - fi -done -``` - -For flagged files: review which sections are reference content (templates, long -YAML, large tables) and split them to `references/.md`. - -### Step 6 — Write evolution log entry - -Append to [`references/evolution-log.md`](references/evolution-log.md): - -```markdown -## YYYY-MM-DD — Session: - -**Skills updated:** -**Shared skills created/updated:** -**Agents split:** -**Summary:** <1-2 sentences> -``` - ---- - -## Shared Skill Extraction Workflow - -When promoting content to a new shared skill: - -1. Create `.github/agents//` directory -2. Create `SKILL.md` with YAML frontmatter (`name` must match directory exactly) -3. Move the content from source agent(s) to the new SKILL.md -4. Replace removed content with a reference link: - ```markdown - See [`..//SKILL.md`](..//SKILL.md). - ``` -5. Update `README.md` shared skills inventory table -6. Add entry to evolution log - ---- - -## Agent Health Indicators - -| Indicator | Threshold | Action | -|-----------|-----------|--------| -| SKILL.md line count | > 400 lines | Split to references/ | -| KNOWLEDGE section entries | > 15 bullets | Consider dedicated references/knowledge.md | -| Shared pattern in N agents | N ≥ 2 | Extract to shared skill | -| References/ file line count | > 300 lines | Review for further split | -| Stale KNOWLEDGE entry | > 90 days no updates | Review if still accurate | - ---- - -## Naming Rules - -### New shared skill names - -- lowercase, hyphens only (e.g. `keycloak-integration`, `mariadb-patterns`) -- must match the directory name exactly -- ≤ 64 characters - -### New knowledge entries - -``` -YYYY-MM-DD: [ProjectCode] -``` - -Project codes: `HNW`, `DSC`, `DSCM`, `TEMPLATE`, or feature branch name. - ---- - -## EVOLUTION_KNOWLEDGE - -> Append new meta-learnings about the agent system itself here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: [TEMPLATE] Initial agent team migrated from flat .agent.md format to Agent Skills SKILL.md directory format. 4 shared skills extracted: ai-session-files, git-conventions, bc-gov-emerald, containerfile-standards. -- 2026-03-04: [TEMPLATE] Added AgentEvolver-inspired improvements: pre-session knowledge retrieval (Step 0), causal CAUSE/FIX annotation format, and cross-session pattern scanning in Step 4. diff --git a/.github/agents/agent-evolution/references/evolution-log.md b/.github/agents/agent-evolution/references/evolution-log.md deleted file mode 100644 index a0427e0..0000000 --- a/.github/agents/agent-evolution/references/evolution-log.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Evolution Log - -Records all changes to the agent skill library. -Maintained by `agent-evolution` at session end. - ---- - -## Format - -``` -## YYYY-MM-DD — Session: - -**Skills updated:** -**Shared skills created/updated:** -**Agents split:** -**Summary:** <1-2 sentences> -``` - ---- - -## 2026-02-27 — Session: agent-skills-migration - -**Skills updated:** all -**Shared skills created/updated:** ai-session-files, git-conventions, bc-gov-emerald, containerfile-standards (all new) -**Agents split:** diagram-generation (plantuml-templates.md), bc-gov-devops (networkpolicy-patterns.md) -**Summary:** Migrated entire agent team from custom flat `.agent.md` format to Agent Skills open -standard (`SKILL.md` directory per skill). Extracted 4 shared skills from cross-cutting content. -Created self-learning `agent-evolution` agent. Agent team is now 9 specialised skills + 4 shared -skills = 13 total SKILL.md directories. Old `*.agent.md` flat files deleted. diff --git a/.github/agents/ai-session-files/SKILL.md b/.github/agents/ai-session-files/SKILL.md deleted file mode 100644 index 5219049..0000000 --- a/.github/agents/ai-session-files/SKILL.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -name: ai-session-files -description: Maintains the AI collaboration audit trail in every project — WORKLOG.md, CHANGES.csv, COMMANDS.sh, and COMMIT_INFO.txt. Use when ending a session, writing session logs, or when any of the files under AI/ need to be created or updated. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: All rloisell/ and bcgov-c/ repositories that follow the standard AI/ directory layout. ---- - -# AI Session Files - -Shared skill — referenced by `session-workflow` and any agent that needs to update -the AI collaboration audit trail. Every project maintains these four files under `AI/`. - -## File Reference - -| File | Updated | Purpose | -|------|---------|---------| -| `AI/WORKLOG.md` | End of session | Narrative log of all actions taken | -| `AI/CHANGES.csv` | Every file create/modify/delete | Machine-readable change record | -| `AI/COMMANDS.sh` | Every significant shell command | Reproducible command history | -| `AI/COMMIT_INFO.txt` | After every commit or push | Branch, hash, outcome record | - ---- - -## WORKLOG.md — Format - -Prepend a new dated section at the top of Session History (newest first): - -```markdown -## YYYY-MM-DD — Session N: - -### Actions -- - -### Files Changed -- `path/to/file.ext` — - -### Commands Run -- `` — - -### Decisions Made -- - -### Outstanding / Carry-over -- -``` - -Keep the document under ~600 lines. Condense early history into summaries rather -than deleting — the full narrative lives here. - ---- - -## CHANGES.csv — Format - -Append one row per file action (create, modify, delete). No header row required -once the file exists. - -```csv -YYYY-MM-DD,created,src/Project.Api/Entities/NetworkTest.cs,Added NetworkTest entity for Quartz scheduler -YYYY-MM-DD,modified,.github/workflows/build-and-test.yml,Expanded path filter to include .github/** -YYYY-MM-DD,deleted,src/Project.Api/Migrations/00001_OldMigration.cs,Replaced by corrected migration -``` - -Fields: `date`, `action` (`created`|`modified`|`deleted`), `relative/path/to/file`, `reason` - ---- - -## COMMANDS.sh — Format - -Append a dated comment header, then the significant commands run in that session: - -```bash -# YYYY-MM-DD — Session N — -dotnet ef migrations add InitialCreate --project src/Project.Api -dotnet ef database update --project src/Project.Api -git add -A && git commit -m "feat: add NetworkTest entity and migration" -git push origin feat/network-test-entity -gh pr create --title "feat: add NetworkTest entity and migration" --base main -gh pr merge 5 --squash --delete-branch -``` - -Include: migrations, git operations, `gh` CLI commands, `oc` commands, builds, test runs. -Omit: trivial file reads, `ls`, `pwd`. - ---- - -## COMMIT_INFO.txt — Format - -Append one line per significant commit or push: - -``` -YYYY-MM-DD [pushed: yes/no] -``` - -Example: -``` -2026-02-27 a3f9c2d feat/network-test feat: add NetworkTest entity pushed: yes -2026-02-27 1eb68b3 main chore: squash merge PR #16 pushed: yes -``` diff --git a/.github/agents/bc-gov-devops/SKILL.md b/.github/agents/bc-gov-devops/SKILL.md deleted file mode 100644 index 2b02461..0000000 --- a/.github/agents/bc-gov-devops/SKILL.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -name: bc-gov-devops -description: BC Government Emerald OpenShift deployment patterns — Artifactory image registry setup, Helm chart requirements, health check standards, Common SSO authentication integration, OpenShift oc command reference, secrets management, and ArgoCD GitOps CRDs. Use when deploying, configuring, or troubleshooting services on the Emerald OpenShift platform. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: Emerald OpenShift 4.x. BC Gov GitHub Actions. Requires Artifactory account from BC Gov DevExchange. ---- - -# BC Gov DevOps Agent - -Drives deployment onto the BC Government Emerald OpenShift platform. - -**Shared skills referenced by this agent:** -- Container image standards → [`../containerfile-standards/SKILL.md`](../containerfile-standards/SKILL.md) -- AVI InfraSettings, DataClass labels, NetworkPolicy model, StorageClass → [`../bc-gov-emerald/SKILL.md`](../bc-gov-emerald/SKILL.md) -- Full NetworkPolicy YAML examples → [`references/networkpolicy-patterns.md`](references/networkpolicy-patterns.md) - ---- - -## Platform Reference - -| Cluster | API URL | OIDC Issuer | -|---------|---------|-------------| -| Gold | `https://api.gold.devops.gov.bc.ca:6443` | `https://loginproxy.gov.bc.ca/auth/realms/standard` | -| Gold DR | `https://api.golddr.devops.gov.bc.ca:6443` | same | -| Silver | `https://api.silver.devops.gov.bc.ca:6443` | `https://loginproxy.gov.bc.ca/auth/realms/standard` | - -**Namespace pattern:** `-` (e.g. `be808f-dev`) - ---- - -## Artifactory Setup - -1. Request Artifactory service account at -2. Store credentials as GitHub Actions secrets: - - `ARTIFACTORY_URL` = `artifacts.developer.gov.bc.ca` - - `ARTIFACTORY_SERVICE_ACCOUNT` = service account name - - `ARTIFACTORY_SERVICE_ACCOUNT_TOKEN` = token -3. In workflow, use the Artifactory URL as the registry prefix — see - `../ci-cd-pipeline/SKILL.md` for full build/push steps. - ---- - -## ag-helm Helm Library Chart (BC Gov AG standard) - -`ag-helm-templates` is the shared Helm library used across BC Gov AG ministry projects. -Consume it via OCI in your chart's `Chart.yaml`: - -```yaml -# gitops-repo/charts//Chart.yaml -dependencies: - - name: ag-helm-templates - version: "" - repository: "oci://ghcr.io/bcgov-c/helm" -``` - -If the registry requires auth: -```bash -echo $GITHUB_TOKEN | helm registry login ghcr.io -u --password-stdin -helm dependency update ./charts/ -``` - -**Templates provided by ag-helm:** - -| Template | Description | -|----------|-------------| -| `ag-template.deployment` | Deployment with OpenShift SCC awareness | -| `ag-template.statefulset` | StatefulSet | -| `ag-template.job` | Job with TTL | -| `ag-template.service` | Service | -| `ag-template.serviceaccount` | ServiceAccount | -| `ag-template.route.openshift` | OpenShift Route with AVI annotation enforcement | -| `ag-template.networkpolicy` | Intent-based NetworkPolicy (recommended) | -| `ag-template.hpa` | HPA (autoscaling/v2) | -| `ag-template.pdb` | PodDisruptionBudget | -| `ag-template.pvc` | PersistentVolumeClaim | - -**Required `values.yaml` root keys when using ag-helm:** -```yaml -global: - openshift: true # enables OpenShift SCC mode — do NOT omit on Emerald - -project: # becomes ApplicationGroup label prefix -registry: artifacts.developer.gov.bc.ca -``` - -**`global.openshift: true` behaviour:** -- Default container `securityContext` does **not** pin `runAsUser`/`runAsGroup` — OpenShift SCC assigns runtime UID/GID -- Adds `checkov.io/skip999: CKV_K8S_40=OpenShift SCC assigns runtime UID/GID` annotation to suppress Checkov false-positive -- Still enforces: `runAsNonRoot: true`, `allowPrivilegeEscalation: false`, `readOnlyRootFilesystem: true`, `capabilities.drop: [ALL]` - -> When NOT using the ag-helm library (raw YAML Helm charts), manually include -> `global.openshift: true` in `values.yaml` and omit `runAsUser`/`runAsGroup` from -> your pod `securityContext` — OpenShift SCC handles them. - ---- - -## Policy-as-Code Gate - -Before deploying to any environment, validate rendered Helm output against policy tools. - -```bash -# 1. Render Helm to a single YAML file -helm template myapp ./charts/ --values ./deploy/dev_values.yaml --debug > rendered.yaml - -# 2. Run policy checks -datree test rendered.yaml --policy-config cd/policies/datree-policies.yaml -polaris audit --config cd/policies/polaris.yaml --format pretty rendered.yaml -kube-linter lint rendered.yaml --config cd/policies/kube-linter.yaml -conftest test rendered.yaml --policy cd/policies --all-namespaces --fail-on-warn -``` - -Policy check sources (bcgov/ag-devops): -- Rego rules: `cd/policies/network-policies.rego` — denies accidental allow-all NetworkPolicy shapes -- Datree config: `cd/policies/datree-policies.yaml` -- Polaris config: `cd/policies/polaris.yaml` -- kube-linter config: `cd/policies/kube-linter.yaml` - -Key Rego denials (from `cd/policies/network-policies.rego`): -- Egress rules without `to` (would allow all destinations) -- Egress rules without `ports` (would allow all ports) -- Wildcard peers inside `from/to` (empty objects or `podSelector: {}`) - ---- - -## Helm Chart Requirements - -Every service needs a Helm chart in `charts//`. - -**Required files:** -``` -charts// - Chart.yaml - values.yaml ← per-service defaults - values-dev.yaml ← dev environment overrides - values-test.yaml - values-prod.yaml - templates/ - deployment.yaml - service.yaml - route.yaml - networkpolicy.yaml ← required; see references/networkpolicy-patterns.md - _helpers.tpl -``` - -**Chart.yaml minimum:** -```yaml -apiVersion: v2 -name: -description: -type: application -version: 0.1.0 -appVersion: "1.0.0" -``` - -For AVI InfraSettings and `app.kubernetes.io/part-of` + DataClass labels, -see [`../bc-gov-emerald/SKILL.md`](../bc-gov-emerald/SKILL.md). - ---- - -## Health Checks - -Every deployment must include both probes: - -```yaml -livenessProbe: - httpGet: - path: /health/live - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 30 - failureThreshold: 3 - -readinessProbe: - httpGet: - path: /health/ready - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 - failureThreshold: 3 -``` - -**.NET health check setup:** -```csharp -builder.Services.AddHealthChecks() - .AddDbContextCheck("database"); - -app.MapHealthChecks("/health/live"); -app.MapHealthChecks("/health/ready", new HealthCheckOptions -{ - Predicate = check => check.Tags.Contains("ready") || check.Name == "database" -}); -``` - ---- - -## Common SSO Authentication - -BC Gov uses DIAM/Keycloak via `loginproxy.gov.bc.ca`. See `jag-diam-documentation/` in the workspace for detailed OIDC flows. - -**Realms:** -| Realm | Use | -|-------|-----| -| `standard` | Production services | -| `onestopauth` | Existing IDIR / BCeID | -| `onestopauth-basic` | BCeID Basic | -| `onestopauth-business` | BCeID Business | - -**React frontend — OIDC PKCE config:** -```json -{ - "authority": "https://loginproxy.gov.bc.ca/auth/realms/standard", - "client_id": "", - "redirect_uri": "https:///callback", - "response_type": "code", - "scope": "openid profile email" -} -``` - ---- - -## OpenShift `oc` Command Reference - -```bash -# Login -oc login --token= --server=https://api.gold.devops.gov.bc.ca:6443 - -# Switch project -oc project - - -# List running pods -oc get pods -n - -# Pod logs -oc logs -n --tail=100 - -# Describe pod (events + probe failures) -oc describe pod -n - -# Exec into pod -oc exec -it -n -- /bin/sh - -# Force rollout -oc rollout restart deployment/ -n - -# Watch rollout -oc rollout status deployment/ -n - -# Scale down for maintenance -oc scale deployment/ --replicas=0 -n -``` - ---- - -## Secrets Management - -**Never commit secrets.** Store in OpenShift Secrets, reference via env vars. - -```yaml -# In Helm deployment.yaml -env: - - name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: -db-secret - key: password - - name: KEYCLOAK_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: -keycloak-secret - key: client-secret -``` - -```bash -# Create secret in namespace -oc create secret generic -db-secret \ - --from-literal=password= \ - -n -``` - ---- - -## ArgoCD Application CRD - -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: - - namespace: openshift-gitops -spec: - project: default - source: - repoURL: https://github.com//gitops-be.git - targetRevision: HEAD - path: environments// - helm: - valueFiles: - - values.yaml - - values-.yaml - destination: - server: https://kubernetes.default.svc - namespace: - - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=false -``` - ---- - -## Deployment Checklist - -- [ ] Containerfile at `src//Containerfile` — see `containerfile-standards` -- [ ] Port 8080 used throughout -- [ ] Non-root `USER 1001` in Containerfile -- [ ] Health probes configured (`/health/live`, `/health/ready`) -- [ ] AVI InfraSettings set (haproxy.router.openshift.io annotations) -- [ ] DataClass label present (`app.kubernetes.io/part-of`) -- [ ] NetworkPolicy: default-deny + targeted allow rules committed -- [ ] Artifactory secrets in namespace -- [ ] ArgoCD Application CRD syncing - ---- - -## PLATFORM_KNOWLEDGE - -> Append new Emerald / OpenShift / Helm discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: ArgoCD auto-sync prune=true removes orphaned resources when Helm chart is updated — confirm before enabling in prod. -- 2026-02-27: OpenShift Routes require `haproxy.router.openshift.io/timeout` annotation if API calls can exceed 30s default. -- 2026-02-27: ServiceAccount needs `anyuid` SCC waiver if running as non-root UID not in OpenShift's allowed range — prefer `nonroot-v2` SCC to `anyuid`. -- 2026-02-28: `global.openshift: true` must be set in Helm values for Emerald — prevents SC UID/GID mismatch with SCC and suppresses false Checkov CKV_K8S_40 warnings. -- 2026-02-28: ag-helm library chart OCI path is `oci://ghcr.io/bcgov-c/helm` (bcgov-c org, not bcgov). Auth via `helm registry login ghcr.io`. -- 2026-02-28: Router NetworkPolicy label should be `ingresscontroller.operator.openshift.io/deployment-ingresscontroller: default` for specificity — broader `network.openshift.io/policy-group: ingress` may also work but confirm with cluster operator. -- 2026-02-28: `dataclass-public` is the correct AVI annotation value for internet-facing Routes (public VIP). Only medium/high/public have registered VIPs on Emerald. -- 2026-02-28: Prod image references should use SHA digest (`image.digest: sha256:...`) rather than mutable tags to ensure pinned deployments. diff --git a/.github/agents/bc-gov-devops/references/networkpolicy-patterns.md b/.github/agents/bc-gov-devops/references/networkpolicy-patterns.md deleted file mode 100644 index 8741ec6..0000000 --- a/.github/agents/bc-gov-devops/references/networkpolicy-patterns.md +++ /dev/null @@ -1,270 +0,0 @@ -# NetworkPolicy Patterns - -Reference NetworkPolicy patterns for BC Government Emerald OpenShift deployments. -Source: [bcgov/ag-devops](https://github.com/bcgov/ag-devops) (authoritative AG ministry DevOps repo). - -For the high-level NetworkPolicy model (why these are required), see -[`../bc-gov-emerald/SKILL.md`](../bc-gov-emerald/SKILL.md). - ---- - -## Recommended: ag-helm Intent-Based API - -When consuming the `ag-helm-templates` library (see `bc-gov-devops/SKILL.md` for setup), -create NetworkPolicies using the intent-based API — avoids accidental allow-all shapes -that are rejected by the Rego policy gate. - -### Frontend policy (ingress from router + egress to backend) - -```tpl -{{- $np := dict "Values" .Values -}} -{{- $_ := set $np "ApplicationGroup" .Values.project -}} -{{- $_ := set $np "Name" "frontend" -}} -{{- $_ := set $np "Namespace" $.Release.Namespace -}} - -{{- $_ := set $np "PolicyTypes" (list "Ingress" "Egress") -}} - -{{- $_ := set $np "AllowIngressFrom" (dict - "ports" (list 8080) - "namespaces" (list (dict - "name" "openshift-ingress" - "podSelector" (dict "matchLabels" (dict - "ingresscontroller.operator.openshift.io/deployment-ingresscontroller" "default" - )) - )) -) -}} - -{{- $_ := set $np "AllowEgressTo" (dict - "apps" (list (dict - "name" "web-api" - "ports" (list (dict "port" 8080 "protocol" "TCP")) - )) -) -}} - -{{ include "ag-template.networkpolicy" $np }} -``` - -### API / Backend policy (ingress from frontend + router; egress to DB + external) - -```tpl -{{- $np := dict "Values" .Values -}} -{{- $_ := set $np "ApplicationGroup" .Values.project -}} -{{- $_ := set $np "Name" "web-api" -}} -{{- $_ := set $np "Namespace" $.Release.Namespace -}} - -{{- $_ := set $np "PolicyTypes" (list "Ingress" "Egress") -}} - -{{- $_ := set $np "AllowIngressFrom" (dict - "ports" (list 8080) - "apps" (list (dict "name" "frontend")) - "namespaces" (list (dict - "name" "openshift-ingress" - "podSelector" (dict "matchLabels" (dict - "ingresscontroller.operator.openshift.io/deployment-ingresscontroller" "default" - )) - )) -) -}} - -{{- $_ := set $np "AllowEgressTo" (dict - "apps" (list (dict - "name" "postgresql" - "ports" (list (dict "port" 3306 "protocol" "TCP")) - )) -) -}} - -{{ include "ag-template.networkpolicy" $np }} -``` - -### Database policy (ingress from API only) - -```tpl -{{- $np := dict "Values" .Values -}} -{{- $_ := set $np "ApplicationGroup" .Values.project -}} -{{- $_ := set $np "Name" "postgresql" -}} -{{- $_ := set $np "Namespace" $.Release.Namespace -}} - -{{- $_ := set $np "PolicyTypes" (list "Ingress") -}} - -{{- $_ := set $np "AllowIngressFrom" (dict - "ports" (list 3306) - "apps" (list (dict "name" "web-api")) -) -}} - -{{ include "ag-template.networkpolicy" $np }} -``` - -> **Rego denial rules**: the policy gate in `cd/policies/network-policies.rego` denies: -> - Egress rules without `to` (allow-all destination) -> - Egress rules without `ports` (allow-all ports) -> - Wildcard peers in `from`/`to` (empty `podSelector: {}`) -> The intent API above avoids these by construction. - ---- - -## Alternative: Raw NetworkPolicy YAML - -Use raw YAML when not consuming the ag-helm library. Apply as Helm templates in -`charts//templates/networkpolicy.yaml`. - ---- - -## Default Deny (apply first to every namespace) - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: default-deny-all - namespace: {{ .Values.namespace }} -spec: - podSelector: {} - policyTypes: - - Ingress - - Egress -``` - -Apply `default-deny-all` **before** any pod-specific allow policies. - ---- - -## Allow Ingress from OpenShift Router - -Use the specific ingress controller label (preferred) or the broader namespace label. - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-from-openshift-ingress - namespace: {{ .Values.namespace }} -spec: - podSelector: - matchLabels: - app: {{ include ".fullname" . }}-frontend - ingress: - - from: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: openshift-ingress - podSelector: - matchLabels: - # Specific label — confirm with: oc -n openshift-ingress get pods --show-labels - ingresscontroller.operator.openshift.io/deployment-ingresscontroller: default - policyTypes: - - Ingress -``` - ---- - -## Allow Frontend → API - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-frontend-to-api - namespace: {{ .Values.namespace }} -spec: - podSelector: - matchLabels: - app: {{ include ".fullname" . }}-api - ingress: - - from: - - podSelector: - matchLabels: - app: {{ include ".fullname" . }}-frontend - ports: - - protocol: TCP - port: 8080 - policyTypes: - - Ingress -``` - ---- - -## Allow API → Database - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-api-to-database - namespace: {{ .Values.namespace }} -spec: - podSelector: - matchLabels: - app: {{ include ".fullname" . }}-database - ingress: - - from: - - podSelector: - matchLabels: - app: {{ include ".fullname" . }}-api - ports: - - protocol: TCP - port: 3306 - policyTypes: - - Ingress -``` - ---- - -## Allow Egress to External (HTTPS only) - -Allow pods to call external services (Keycloak, Artifactory, GitHub, etc.). - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-egress-https - namespace: {{ .Values.namespace }} -spec: - podSelector: {} - egress: - - ports: - - protocol: TCP - port: 443 - - protocol: TCP - port: 80 - policyTypes: - - Egress -``` - ---- - -## Allow DNS Resolution - -Required for hostname-based service discovery. - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-dns - namespace: {{ .Values.namespace }} -spec: - podSelector: {} - egress: - - ports: - - protocol: UDP - port: 53 - - protocol: TCP - port: 53 - policyTypes: - - Egress -``` - ---- - -## Complete Minimal Policy Set (copy-paste starting point) - -For most two-tier (frontend + API) services, apply ALL of the following: - -1. `default-deny-all` -2. `allow-from-openshift-ingress` -3. `allow-frontend-to-api` -4. `allow-api-to-database` (if DB in same namespace) -5. `allow-egress-https` -6. `allow-dns` - -For API-only services, omit (2) and (3), add a direct ingress-from-router rule targeting the API pods. diff --git a/.github/agents/bc-gov-emerald/SKILL.md b/.github/agents/bc-gov-emerald/SKILL.md deleted file mode 100644 index 3ad0305..0000000 --- a/.github/agents/bc-gov-emerald/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: bc-gov-emerald -description: BC Government Emerald OpenShift platform standards: namespace conventions, AVI InfraSettings route annotations, DataClass pod labels, NetworkPolicy default-deny model, and StorageClass requirements. Use when creating or reviewing Helm charts, Routes, NetworkPolicies, or any deployment manifest for BC Gov Emerald OpenShift. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: BC Gov Emerald OpenShift cluster. Projects in bcgov-c/ and rloisell/ namespaces (be808f family). ---- - -# BC Gov Emerald Platform Standards - -Shared skill — referenced by `bc-gov-devops` and `ci-cd-pipeline`. Contains -platform-level rules that apply to every workload deployed to BC Gov Emerald. - ---- - -## Namespace Convention - -``` --dev ← development --test ← staging / QA --prod ← production --tools ← CI/CD tooling, Artifactory, Vault -``` - ---- - -## AVI InfraSettings — Route Annotation - -Controls which VIP pool handles the Route. **Get this wrong and traffic silently drops.** - -| Annotation value | VIP | When to use | -|-----------------|-----|-------------| -| `dataclass-medium` | Private VIP — VPN only | ✅ All internal workloads (default) | -| `dataclass-high` | Private VIP — sensitive data | Higher-trust internal workloads | -| `dataclass-public` | Public internet VIP | Internet-facing routes with public exposure | -| `dataclass-low` | ⚠️ NO VIP on Emerald | **NEVER USE** — DNS resolves but `ERR_EMPTY_RESPONSE` | - -```yaml -# Required on every OpenShift Route -metadata: - annotations: - aviinfrasetting.ako.vmware.com/name: "dataclass-medium" -``` - -AKO re-adds this annotation within ~15 seconds if removed — always keep it in Helm values. - ---- - -## DataClass Pod Label - -Pod `DataClass` label **must match** the AVI annotation suffix. - -```yaml -podLabels: - DataClass: "Medium" # matches "dataclass-medium" annotation -``` - -Mismatch rule: `DataClass: Low` + `dataclass-medium` route → SDN silently drops traffic. - -### Internet-Ingress label -```yaml -podLabels: - Internet-Ingress: "DENY" # default — correct for all internal services - # Internet-Ingress: "ALLOW" # only if reachable from public internet via Public VIP -``` - ---- - -## NetworkPolicy Model - -Emerald default-denies **both Ingress AND Egress**. Every traffic flow needs two policies. - -| Flow | Policy needed | -|------|--------------| -| Router → Frontend | Ingress on Frontend | -| Router → API | Ingress on API | -| Frontend → API | Ingress on API **+** Egress from Frontend | -| API → DB | Ingress on DB **+** Egress from API | -| Any pod → DNS | Egress UDP+TCP 53 on every pod | - -**Common mistake**: defining only Ingress `api-to-db` but forgetting the API Egress rule → -TCP connect timeout at startup. - -See `bc-gov-devops/references/networkpolicy-patterns.md` for full YAML examples. - ---- - -## OpenShift Mode in Helm (`global.openshift: true`) - -Set this in every Helm chart's `values.yaml` targeting Emerald: - -```yaml -global: - openshift: true -``` - -Effect: -- Deployment/Job pod `securityContext` does **not** pin `runAsUser`/`runAsGroup` — OpenShift SCC assigns runtime UID/GID -- Adds `checkov.io/skip999: CKV_K8S_40=...` annotation to suppress Checkov false-positive -- Still enforces `runAsNonRoot`, `allowPrivilegeEscalation: false`, `readOnlyRootFilesystem: true`, capabilities drop ALL - -> ⚠️ Omitting `global.openshift: true` when using the ag-helm library chart causes the -> deployment template to pin `runAsUser: 10001` which may conflict with the namespace's SCC. - ---- - -## StorageClass - -```yaml -storageClassName: netapp-file-standard # ✅ correct for Emerald PVCs -# netapp-block-standard # ❌ single-pod access only — avoid -``` - ---- - -## DNS Split-Tunneling - -Route hostnames (`*.apps.emerald.devops.gov.bc.ca`) resolve only via BC Gov VPN DNS. -Local / home DNS returns NXDOMAIN. Ensure VPN client routes this domain through VPN DNS -before debugging route connectivity issues. diff --git a/.github/agents/bc-gov-iam/SKILL.md b/.github/agents/bc-gov-iam/SKILL.md deleted file mode 100644 index f6ae251..0000000 --- a/.github/agents/bc-gov-iam/SKILL.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -name: bc-gov-iam -description: BC Government Identity and Access Management — DIAM/Keycloak OIDC PKCE integration, realm selection, React oidc-client-ts setup, token lifecycle, backchannel logout, Common SSO vs DIAM comparison, and user attribute mapping. Use when implementing authentication, configuring an OIDC client, troubleshooting token refresh, or choosing between identity providers. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: DIAM Keycloak. Common SSO (loginproxy.gov.bc.ca). React oidc-client-ts v3+. .NET 10 ASP.NET Core. ---- - -# BC Gov IAM Agent - -Implements authentication and authorisation for BC Government applications using DIAM (Digital -Identity and Access Management) or Common SSO Keycloak. - -**Related skills:** -- Security controls → [`../security-architect/SKILL.md`](../security-architect/SKILL.md) -- Vault for storing OIDC client secrets → [`../vault-secrets/SKILL.md`](../vault-secrets/SKILL.md) - ---- - -## Identity Provider Reference - -| Provider | Base URL | Realm | Use When | -|----------|----------|-------|----------| -| Common SSO (IDIR + BCeID) | `https://loginproxy.gov.bc.ca` | `standard` | Internal staff / business users | -| DIAM | `https://diam-dev.gov.bc.ca` / `diam.gov.bc.ca` | Project-specific | Ministry-specific brokered identity | -| IDIR (direct) | `https://loginproxy.gov.bc.ca` | `idir` | IDIR only, no BCeID needed | -| BCeID (direct) | `https://loginproxy.gov.bc.ca` | `bceidbasic` / `bceidbusiness` | BCeID only | - -DIAM brokers to Common SSO as a federation layer. Most new projects use **Common SSO `standard`** -unless there is a specific requirement for DIAM's additional brokering or custom attribute mapping. - -### OIDC Discovery URL -``` -https://loginproxy.gov.bc.ca/auth/realms/standard/.well-known/openid-configuration -``` - ---- - -## PKCE Flow Summary - -``` -Browser React App Keycloak - | | | - |── click Login ──> | | - | generate code_verifier | - | hash → code_challenge | - | |── GET /auth? | - | | client_id | - | | code_challenge ──> | - | | redirect_uri + auth_code - | |<── auth_code ─────────| - | |── POST /token | - | | code + code_verifier| - | |──────────────────────>| - | |<── access_token | - | | refresh_token ─────| -``` - -No `client_secret` required for public clients (SPA). The `code_verifier`/`code_challenge` pair -replaces the implicit flow's reliance on a trusted origin. - ---- - -## React Setup (`oidc-client-ts`) - -### Install -```bash -npm install oidc-client-ts react-oidc-context -``` - -### Auth config -```js -// src/auth/authConfig.js — OIDC UserManager config -// Author: Ryan Loiselle | Date: - -export const oidcConfig = { - authority: process.env.VITE_OIDC_AUTHORITY, - // e.g. https://loginproxy.gov.bc.ca/auth/realms/standard - client_id: process.env.VITE_OIDC_CLIENT_ID, - redirect_uri: `${window.location.origin}/callback`, - post_logout_redirect_uri: `${window.location.origin}/`, - response_type: 'code', - scope: 'openid profile email', - automaticSilentRenew: true, - silent_redirect_uri: `${window.location.origin}/silent-callback.html`, - monitorSession: true, - filterProtocolClaims: true, - loadUserInfo: true, -}; -``` - -### Provider wrapping -```jsx -// src/main.jsx -import { AuthProvider } from 'react-oidc-context'; -import { oidcConfig } from './auth/authConfig'; - -root.render( - - - -); -``` - -### Callback page -```jsx -// src/pages/Callback.jsx — redirect landing page -import { useAuth } from 'react-oidc-context'; -import { useNavigate } from 'react-router-dom'; -import { useEffect } from 'react'; - -export default function Callback() { - const auth = useAuth(); - const navigate = useNavigate(); - - useEffect(() => { - if (!auth.isLoading && !auth.error) navigate('/'); - }, [auth.isLoading, auth.error]); - - if (auth.error) return
Login error: {auth.error.message}
; - return
Logging in…
; -} -``` - -### Silent renew page -Create `/public/silent-callback.html`: -```html - - - - - -``` - -### Using the token in API calls -```js -// src/api/AuthConfig.js — axios interceptor to inject access token -import { getUser } from 'oidc-client-ts'; -import axios from 'axios'; - -const apiClient = axios.create({ baseURL: window.__env__?.apiUrl }); - -apiClient.interceptors.request.use(async (config) => { - const user = getUser({ authority: import.meta.env.VITE_OIDC_AUTHORITY, - client_id: import.meta.env.VITE_OIDC_CLIENT_ID }); - if (user?.access_token) { - config.headers.Authorization = `Bearer ${user.access_token}`; - } - return config; -}); - -export default apiClient; -``` - ---- - -## .NET API — JWT Validation - -### Package -```bash -dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer -``` - -### Program.cs -```csharp -// OIDC JWT bearer authentication — validates tokens from Keycloak -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.Authority = builder.Configuration["Oidc:Authority"]; - // e.g. https://loginproxy.gov.bc.ca/auth/realms/standard - options.Audience = builder.Configuration["Oidc:ClientId"]; - options.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ClockSkew = TimeSpan.FromSeconds(60), - }; - }); - -builder.Services.AddAuthorization(); -// ... -app.UseAuthentication(); -app.UseAuthorization(); -``` - -### Extracting claims -```csharp -// returns the sub claim (Keycloak user ID) -private static Guid? GetUserId(ClaimsPrincipal user) - => Guid.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out var id) - ? id : null; -``` - ---- - -## Keycloak Client Registration - -### Public client (React SPA) -| Setting | Value | -|---------|-------| -| Access type | `public` | -| Standard flow | `ON` | -| Implicit flow | `OFF` | -| Valid redirect URIs | `https://--dev.apps.emerald.devops.gov.bc.ca/callback` | -| Post logout redirect URIs | `https://--dev.apps.emerald.devops.gov.bc.ca/` | -| Web origins | same as redirect (no `+` wildcard in prod) | -| PKCE method | `S256` | - -### Confidential client (API-to-API) -| Setting | Value | -|---------|-------| -| Access type | `confidential` | -| Service accounts | `ON` | -| `client_secret` | Store in Vault at `secret///oidc-client-secret` | - ---- - -## Backchannel Logout (.NET API) - -```csharp -// POST /logout/backchannel — handles Keycloak backchannel logout token -[HttpPost("/logout/backchannel")] -[AllowAnonymous] -[Consumes("application/x-www-form-urlencoded")] -public async Task BackchannelLogout([FromForm] string logout_token) -{ - // 1. Validate logout_token JWT: issuer, audience, iat, jti - // 2. Extract 'sub' or 'sid' claim - // 3. Invalidate any server-side session or cached token for that sub/sid - // 4. Return 200 OK or 400 Bad Request - _logger.LogInformation("Backchannel logout received for sub={Sub}", sub); - return Ok(); -} -``` - ---- - -## IAM_KNOWLEDGE - -```yaml -confirmed_facts: - - "Common SSO 'standard' realm brokers IDIR, BCeID Basic, and BCeID Business" - - "PKCE with S256 is mandatory for public clients — implicit flow is disabled on Common SSO" - - "automaticSilentRenew uses a hidden iframe; may fail in Safari with ITP — use refresh_token_grant as fallback" - - "Keycloak sub claim = user's UUID in the realm — use as the canonical user identifier" - - "DIAM adds a JAG-specific brokering layer on top of Common SSO for JAG ministry projects" - - "Vault path for OIDC client secret: secret///oidc-client-secret" -common_pitfalls: - - "do NOT store access_token in localStorage — use oidc-client-ts in-memory store" - - "ClockSkew must be set to at least 30s to tolerate server timing differences with Keycloak" - - "Post logout redirect URI must be registered in Keycloak or the logout will fail silently" - - "Bearer token audience validation: Keycloak issues 'account' audience by default — configure 'Add Audience' mapper" -``` diff --git a/.github/agents/ci-cd-pipeline/SKILL.md b/.github/agents/ci-cd-pipeline/SKILL.md deleted file mode 100644 index a36f6ce..0000000 --- a/.github/agents/ci-cd-pipeline/SKILL.md +++ /dev/null @@ -1,287 +0,0 @@ ---- -name: ci-cd-pipeline -description: Configures and troubleshoots GitHub Actions CI/CD pipelines for BC Government OpenShift projects following ISB EA Option 2 — five-workflow pattern, image tag strategy, yq GitOps updates, Trivy scanning, and pipeline failure triage. Use when creating workflows, diagnosing failures, setting up branch protection, or updating the OpenShift GitOps image tag after a successful build. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: GitHub Actions. Emerald OpenShift platform — see ../bc-gov-emerald/SKILL.md. Containerfile standards — see ../containerfile-standards/SKILL.md. ---- - -# CI/CD Pipeline Agent - -Manages GitHub Actions workflows for BC Government projects on the Emerald OpenShift platform. - -For container registry and Containerfile standards, see -[`../containerfile-standards/SKILL.md`](../containerfile-standards/SKILL.md). - -For AVI InfraSettings, DataClass labels, and NetworkPolicy, see -[`../bc-gov-emerald/SKILL.md`](../bc-gov-emerald/SKILL.md). - ---- - -## Architecture: ISB EA Option 2 - -``` -GitHub (source) ──build──> Artifactory (images) ──gitops──> OpenShift (runtime) - ArgoCD (GitOps) polls a gitops-be repo and applies Helm -``` - -Environments: `dev` → `test` → `prod` -Promotion is controlled by updating the image tag in the GitOps Helm values YAML. - ---- - -## Five-Workflow Pattern - -| File | Trigger | Purpose | -|------|---------|---------| -| `pr-checks.yml` | `pull_request` | Lint, unit tests, Trivy scan | -| `build-dev.yml` | `push: branches: [develop]` | Build + push image → deploy dev | -| `build-test.yml` | `push: branches: [test]` | Build + push image → deploy test | -| `build-prod.yml` | `push: tags: v*` | Build + push image → deploy prod | -| `dependency-review.yml` | `pull_request` | OSSF dependency-review action | - ---- - -## Image Tag Strategy - -``` -///: - -Tags: - PR: pr-- e.g. pr-42-abc1234 - Dev: dev-- e.g. dev-abc1234-87 - Test: test-- e.g. test-abc1234-87 - Prod: e.g. 1.2.3 -``` - ---- - -## Build Workflow Steps - -```yaml -jobs: - build: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Set image tag - id: tag - run: echo "tag=dev-${GITHUB_SHA::7}-${{ github.run_number }}" >> "$GITHUB_OUTPUT" - - - name: Login to Artifactory - uses: docker/login-action@v3 - with: - registry: ${{ secrets.ARTIFACTORY_URL }} - username: ${{ secrets.ARTIFACTORY_SERVICE_ACCOUNT }} - password: ${{ secrets.ARTIFACTORY_SERVICE_ACCOUNT_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - file: src//Containerfile - push: true - tags: ${{ secrets.ARTIFACTORY_URL }}///:${{ steps.tag.outputs.tag }} - - - name: Trivy image scan - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ secrets.ARTIFACTORY_URL }}///:${{ steps.tag.outputs.tag }} - format: table - exit-code: 1 - vuln-type: os,library - severity: CRITICAL,HIGH -``` - ---- - -## GitOps Image Tag Update (yq) - -After a successful build, update the GitOps Helm values file and push: - -```yaml - - name: Update GitOps image tag - run: | - git clone https://x-access-token:${{ secrets.GITOPS_TOKEN }}@github.com//gitops-be.git gitops - cd gitops - yq -i '.image.tag = "${{ steps.tag.outputs.tag }}"' \ - environments/dev//values.yaml - git config user.email "ci@" - git config user.name "GitHub Actions" - git commit -am "ci: update image tag to ${{ steps.tag.outputs.tag }}" - git push -``` - ---- - -## Secrets Required - -| Secret | Description | -|--------|-------------| -| `ARTIFACTORY_URL` | Registry base URL | -| `ARTIFACTORY_SERVICE_ACCOUNT` | Robot account name | -| `ARTIFACTORY_SERVICE_ACCOUNT_TOKEN` | Robot account token | -| `GITOPS_TOKEN` | PAT — `repo` + `workflow` scopes only | -| `OC_SERVER` | OpenShift cluster API URL (optional for direct oc deploy) | -| `OC_TOKEN` | Service account token for direct oc (optional) | - ---- - -## Placeholder Substitution - -When starting from the template, replace all `` values. Find them with: - -```bash -grep -r "<" .github/workflows/ | grep -v ".git" -``` - ---- - -## paths: Filter Discovery (2026-02-28) - -When workflows use `paths:` filters that don't match changed files, the workflow -never triggers. Cause: path in filter doesn't match actual directory structure. - -```bash -# Find all paths filters in workflows -grep -r "paths:" .github/workflows/ - -# Verify the actual paths that exist -find . -name "Containerfile" -not -path "./.git/*" -``` - -Fix: align `paths:` filters with the real directory tree. - ---- - -## Status Check Configuration - -**Branch protection must list the EXACT workflow job name.** - -```bash -# List all job names across all workflows -grep -h "^ [a-z].*:$" .github/workflows/*.yml | sed 's/://g' | sort -u -``` - -In GitHub: Settings → Branches → Require status checks → paste each job name. - ---- - -## PR Copilot Review - -In `.github/workflows/pr-checks.yml`: - -```yaml - - name: Copilot code review - uses: github/copilot-code-review@main - with: - github-token: ${{ secrets.GITHUB_TOKEN }} -``` - -Only runs on pull requests — no `GITHUB_TOKEN` permissions needed beyond default. - ---- - -## Trivy Scan - -```yaml - - name: Trivy scan - uses: aquasecurity/trivy-action@master - with: - image-ref: ':' - format: table - exit-code: 1 - vuln-type: os,library - severity: CRITICAL,HIGH -``` - -CRITICAL/HIGH → pipeline fails. Patch OS packages first (update base image), -then review library versions. - ---- - -## Policy-as-Code Gate - -Run the policy gate **after `helm template` renders successfully** and **before any deploy step**. -Source of truth: [bcgov/ag-devops `cd/policies/`](https://github.com/bcgov/ag-devops/tree/main/cd/policies/). - -### Chart.yaml — OCI dependency for ag-helm (AG ministry projects) - -```yaml -# charts//Chart.yaml -dependencies: - - name: ag-helm-templates - version: "" - repository: "oci://ghcr.io/bcgov-c/helm" -``` - -Pre-step in CI (or developer workstation): -```bash -echo $GITHUB_TOKEN | helm registry login ghcr.io -u --password-stdin -helm dependency update ./charts/ -``` - -### Policy validation step (GitHub Actions) - -```yaml - - name: Render Helm templates - run: | - helm dependency update ./charts/${{ env.APP_NAME }} - helm template ${{ env.APP_NAME }} ./charts/${{ env.APP_NAME }} \ - --values ./deploy/${{ env.ENVIRONMENT }}_values.yaml \ - --debug > rendered.yaml - - - name: Policy-as-code gate - run: | - datree test rendered.yaml \ - --policy-config cd/policies/datree-policies.yaml - polaris audit \ - --config cd/policies/polaris.yaml \ - --format pretty rendered.yaml - kube-linter lint rendered.yaml \ - --config cd/policies/kube-linter.yaml - conftest test rendered.yaml \ - --policy cd/policies \ - --all-namespaces \ - --fail-on-warn -``` - -| Tool | What it checks | Failure action | -|------|---------------|----------------| -| Datree | Kubernetes best practices schema | Block deploy | -| Polaris | Security/reliability checks (CPU/memory limits, probes) | Block deploy | -| kube-linter | Image latest tag, missing labels, privilege escalation | Block deploy | -| conftest / OPA | Rego rules (no allow-all NetworkPolicy shapes, etc.) | Block deploy | - -Key Rego rules (from `cd/policies/network-policies.rego`): -- Deny egress rules missing `to:` (allow-all destination) -- Deny egress rules missing `ports:` (allow-all ports) -- Deny wildcard `from`/`to` peers with empty `podSelector: {}` - ---- - -## Failure Patterns - -| Symptom | Cause | Fix | -|---------|-------|-----| -| Workflow never triggers | `paths:` filter mismatch | Fix paths or remove filter | -| Artifactory auth denied | Token not in secrets | Add/rotate secret | -| Trivy CRITICAL found | Stale base image | Update base image tag in Containerfile | -| yq update silently no-ops | Wrong YAML key path | `yq '.'` to inspect actual key path | -| GitOps push rejected | Token lacks `Contents: write` | Add to PAT or use fine-grained PAT | -| Status check not required | Job name mismatch in branch protection | Run grep; update protection rule | - ---- - -## PIPELINE_KNOWLEDGE - -> Append new CI/CD discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: [HelloNetworkWorld] Paths filter fix — workflow was targeting wrong directory. `grep -r "paths:" .github/workflows/` revealed mismatch. Removing or fixing paths filter resolved silent non-trigger. -- 2026-02-27: [HelloNetworkWorld] Status checks must use exact job ID strings — copy from workflow YAML, not display names. -- 2026-02-28: ag-helm OCI chart at `oci://ghcr.io/bcgov-c/helm` requires `helm registry login ghcr.io` before `helm dependency update` — add as a step before template render. -- 2026-02-28: Policy-as-code gate must run on the output of `helm template`; conftest requires `--all-namespaces --fail-on-warn`; kube-linter config path is `cd/policies/kube-linter.yaml` (source: bcgov/ag-devops). -- 2026-02-28: Prod image reference should prefer `image.digest: sha256:...` over mutable tag — use `docker inspect --format='{{index .RepoDigests 0}}' :` to obtain digest after build. diff --git a/.github/agents/containerfile-standards/SKILL.md b/.github/agents/containerfile-standards/SKILL.md deleted file mode 100644 index 456ade3..0000000 --- a/.github/agents/containerfile-standards/SKILL.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: containerfile-standards -description: Standard Containerfile patterns for .NET API and React/Vite frontend images used in BC Gov projects: port 8080, non-root appuser, HEALTHCHECK, and runtime config.json for frontend API URL. Use when creating or modifying any Containerfile or Dockerfile for a BC Gov project. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: BC Gov Emerald OpenShift. .NET 10 API + Node 22 / React / Vite frontend stack. ---- - -# Containerfile Standards - -Shared skill — referenced by `bc-gov-devops` and `ci-cd-pipeline`. All container -images for BC Gov projects must follow these patterns. - -## Port Rule - -**Always expose port `8080` inside containers.** Never 80, 443, 5000, or 5005. -OpenShift Routes and the AVI load balancer handle TLS externally. - ---- - -## .NET API Containerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /app -COPY . . -RUN dotnet publish -c Release -o /publish - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime -WORKDIR /app -EXPOSE 8080 -ENV ASPNETCORE_URLS=http://+:8080 -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* -RUN groupadd -r appgroup && useradd -r -g appgroup appuser -COPY --from=build /publish . -USER appuser -HEALTHCHECK --interval=30s --timeout=5s \ - CMD curl -f http://localhost:8080/health/live || exit 1 -ENTRYPOINT ["dotnet", ".dll"] -``` - ---- - -## React / Vite Frontend Containerfile - -```dockerfile -FROM node:22-alpine AS build -WORKDIR /app -COPY package*.json ./ -RUN npm ci -COPY . . -RUN npm run build - -FROM nginx:alpine AS runtime -EXPOSE 8080 -RUN apk add --no-cache wget -RUN addgroup -S appgroup && adduser -S appuser -G appgroup -COPY --from=build /app/dist /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf -USER appuser -HEALTHCHECK --interval=30s --timeout=3s \ - CMD wget -q -O /dev/null http://localhost:8080/nginx-health || exit 1 -``` - ---- - -## Runtime Config — Frontend API URL - -**Never** bake `VITE_API_URL` at build time. Use a `/config.json` served by Nginx -so one image works across dev, test, and prod. - -### Nginx location block -```nginx -location = /config.json { - alias /usr/share/nginx/html/config.json; - add_header Cache-Control "no-cache"; -} -``` - -### React app fetch (main.jsx) -```javascript -// top-level await requires build.target: 'esnext' in vite.config.js -const config = await fetch('/config.json').then(r => r.json()).catch(() => ({ - apiUrl: 'http://localhost:5200', -})); -window.__env__ = config; -``` - -The Helm chart writes `config.json` into the container via a ConfigMap at deploy time. - ---- - -## Common Rules - -- Non-root user `appuser` on all containers — `USER appuser` as final instruction -- `cap_drop: [ALL]` and `security_opt: [no-new-privileges: true]` in compose files -- Install `curl` (API) or `wget` (frontend) for health checks -- `HEALTHCHECK` required on every image pointing at `/health/live` or `/nginx-health` diff --git a/.github/agents/diagram-generation/SKILL.md b/.github/agents/diagram-generation/SKILL.md deleted file mode 100644 index 4b0ab0a..0000000 --- a/.github/agents/diagram-generation/SKILL.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -name: diagram-generation -description: Creates and exports architecture diagrams for every project using draw.io, PlantUML, and Mermaid — manages the 10-diagram standard suite required by CODING_STANDARDS.md §7, file organisation, VS Code extension setup, and CLI export commands. Use when creating, updating, or exporting any architecture, sequence, class, state, ERD, or deployment diagram. -metadata: - author: Ryan Loiselle - version: "1.0" ---- - -# Diagram Generation Agent - -Creates and maintains the documentation diagram suite. - -For PlantUML skeleton templates, see -[`references/plantuml-templates.md`](references/plantuml-templates.md). - ---- - -## Tool Selection - -| Use draw.io when... | Use PlantUML when... | Use Mermaid when... | -|---------------------|----------------------|---------------------| -| Architecture, infra, topology | Sequence, class, state, package | Quick inline docs | -| C4 Container/Context | Code-generated UML | GitHub README diagrams | -| ERD / data model | Activity diagrams | Flowcharts in markdown | -| Free-form whiteboard | Any text-based UML | No tool install needed | - -**SVG preferred** over PNG (resolution-independent). Export both when embedded in -markdown viewed in both GitHub and VS Code. - ---- - -## Standard Folder Structure - -``` -diagrams/ - drawio/ - .drawio ← source - svg/.svg ← exported (committed) - plantuml/ - .puml ← source - png/.png ← exported (committed) - data-model/ - .drawio ← ERD / schema source - svg/.svg - png/.png - -docs/diagrams/ - README.md ← Mermaid diagrams inline -``` - ---- - -## Required Diagrams (all 10 required for production-ready features) - -| # | Diagram | UML Type | Format | Location | -|---|---------|----------|--------|----------| -| 1 | System Architecture | Component | draw.io | `drawio/system-architecture.drawio` | -| 2 | Domain Class Model | Class | PlantUML | `plantuml/class-model.puml` | -| 3 | Package / Module Organisation | Package | PlantUML | `plantuml/package-structure.puml` | -| 4 | Use Case Overview | Use Case | PlantUML | `plantuml/use-cases.puml` | -| 5 | Key Sequence Flows *(per feature)* | Sequence | PlantUML | `plantuml/-sequence.puml` | -| 6 | Key Workflows *(per feature)* | Activity | PlantUML | `plantuml/-workflow.puml` | -| 7 | Entity Lifecycle *(non-trivial state)* | State | PlantUML | `plantuml/-state.puml` | -| 8 | Entity-Relationship Diagram | ERD | draw.io | `data-model/erd.drawio` | -| 9 | Physical Database Schema | Schema | draw.io | `data-model/physical-schema.drawio` | -| 10 | Deployment Topology | Deployment | draw.io | `drawio/deployment-topology.drawio` | - -Never remove the 10 base diagrams. Add project-specific diagrams as needed. - ---- - -## VS Code Extensions - -```bash -code --install-extension hediet.vscode-drawio # draw.io native editing -code --install-extension jebbs.plantuml # PlantUML preview -code --install-extension bierner.markdown-mermaid # Mermaid preview -``` - -`.vscode/settings.json`: -```json -{ - "plantuml.render": "PlantUMLServer", - "plantuml.server": "https://www.plantuml.com/plantuml", - "plantuml.exportFormat": "png", - "plantuml.exportOutDir": "diagrams/plantuml/png" -} -``` - ---- - -## Export Commands - -### draw.io CLI - -```bash -brew install --cask drawio # macOS - -# Single file -drawio --export --format svg --embed-diagram --border 10 \ - --output diagrams/drawio/svg/.svg diagrams/drawio/.drawio - -# All files -find diagrams/drawio -name "*.drawio" -not -path "*/svg/*" | while read f; do - name=$(basename "$f" .drawio) - drawio --export --format svg --embed-diagram --border 10 \ - --output "diagrams/drawio/svg/${name}.svg" "$f" -done -``` - -### PlantUML CLI - -```bash -brew install plantuml # macOS - -# Single file -plantuml -tpng -o diagrams/plantuml/png diagrams/plantuml/.puml - -# All files -find diagrams/plantuml -maxdepth 1 -name "*.puml" | while read f; do - plantuml -tpng -o ../png "$f" -done -``` - -### Mermaid (inline — no export needed) - -Mermaid diagrams are written inline in markdown and rendered natively by GitHub. -Preview in VS Code with the `bierner.markdown-mermaid` extension. - -```markdown -```mermaid -graph LR - User -->|HTTPS| Frontend - Frontend -->|REST| API - API -->|SQL| Database -` `` -``` - ---- - -## DIAGRAM_KNOWLEDGE - -> Append new diagram discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: draw.io `.drawio` files are XML — safe in git. Use SVG exports for docs; PNG where SVG not supported. -- 2026-02-27: PlantUML server render (plantuml.com) works without local Java — sufficient for day-to-day preview. Use local CLI for batch export at commit time. diff --git a/.github/agents/diagram-generation/references/plantuml-templates.md b/.github/agents/diagram-generation/references/plantuml-templates.md deleted file mode 100644 index d86cecc..0000000 --- a/.github/agents/diagram-generation/references/plantuml-templates.md +++ /dev/null @@ -1,199 +0,0 @@ -# PlantUML Skeleton Templates - -Reference templates for the 5 standard PlantUML diagram types. -Copy-paste the appropriate skeleton, rename, and fill in your project specifics. - ---- - -## Sequence Diagram - -```plantuml -@startuml -title Feature Name — Key Operation Sequence - -actor User -participant Frontend -participant "API :Controller" as API -participant "Service" as Svc -database "MariaDB" as DB - -User -> Frontend : action -activate Frontend - -Frontend -> API : POST /api/resource (dto) -activate API - -API -> Svc : DoSomethingAsync(dto) -activate Svc - -Svc -> DB : INSERT / SELECT -DB --> Svc : result -Svc --> API : dto -deactivate Svc - -API --> Frontend : 200 OK { ... } -deactivate API - -Frontend --> User : updated UI -deactivate Frontend - -@enduml -``` - ---- - -## Class Diagram - -```plantuml -@startuml -title Domain Class Model — - -package "Domain" { - class Entity { - +Id : Guid - +Name : string - +CreatedAt : DateTime - +UpdatedAt : DateTime? - +IsActive : bool - +Deactivate() : void - } - - class RelatedEntity { - +Id : Guid - +EntityId : Guid - +Value : string - } - - Entity "1" --> "0..*" RelatedEntity : contains -} - -package "API" { - class EntityController { - -_service : IEntityService - +GetAll() : Task - +GetById(id : Guid) : Task - +Create(dto : CreateRequest) : Task - +Update(id : Guid, dto : UpdateRequest) : Task - +Delete(id : Guid) : Task - } - - interface IEntityService { - +GetAllAsync() : Task - +GetByIdAsync(id : Guid) : Task - +CreateAsync(r : CreateRequest) : Task - +UpdateAsync(id : Guid, r : UpdateRequest) : Task - +DeleteAsync(id : Guid) : Task - } - - EntityController --> IEntityService -} - -package "Infrastructure" { - class EntityService { - -_db : ApplicationDbContext - } - - EntityService ..|> IEntityService -} - -@enduml -``` - ---- - -## State Diagram - -```plantuml -@startuml -title Entity Lifecycle — - -[*] --> Draft : created - -Draft --> PendingReview : submit() -Draft --> Cancelled : cancel() - -PendingReview --> Active : approve() -PendingReview --> Draft : requestChanges() -PendingReview --> Cancelled : cancel() - -Active --> Suspended : suspend() -Active --> Completed : complete() - -Suspended --> Active : reinstate() -Suspended --> Cancelled : cancel() - -Completed --> [*] -Cancelled --> [*] - -note right of PendingReview - Notifications sent to - assignee on entry -end note - -@enduml -``` - ---- - -## C4 Context + Container - -```plantuml -@startuml -title C4 — Context - -!define C4Context -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml - -Person(user, "End User", "Uses the system via browser") -Person(admin, "Administrator", "Manages configuration") - -System_Boundary(sys, "") { - System(frontend, "React SPA", "Single page application") - System(api, ".NET 10 API", "REST API") - System(db, "MariaDB", "Relational data store") -} - -System_Ext(keycloak, "DIAM / Keycloak", "Identity provider (OIDC)") -System_Ext(openshift, "Emerald OpenShift", "Container platform") - -Rel(user, frontend, "Uses", "HTTPS 443") -Rel(admin, frontend, "Manages", "HTTPS 443") -Rel(frontend, api, "API calls", "HTTPS 443") -Rel(api, db, "Reads/writes", "TCP 3306") -Rel(frontend, keycloak, "Auth", "OIDC / PKCE") -Rel(api, keycloak, "Token validation", "JWKS") - -@enduml -``` - ---- - -## Mermaid — Architecture Quickview - -```mermaid -graph TB - subgraph "Client" - U[User Browser] - end - subgraph "Emerald OpenShift" - subgraph "Tools Namespace" - GH[GitHub Actions Runner] - end - subgraph "DEV / TEST / PROD Namespace" - FE[React Frontend :8080] - API[.NET API :8080] - DB[(MariaDB :3306)] - end - end - subgraph "External" - DIAM[DIAM / Keycloak] - ACR[Artifactory Registry] - end - - U -->|HTTPS| FE - FE -->|REST| API - API -->|SQL| DB - FE -->|OIDC| DIAM - GH -->|push image| ACR - API -->|pull image| ACR -``` diff --git a/.github/agents/ef-core/SKILL.md b/.github/agents/ef-core/SKILL.md deleted file mode 100644 index 56d12dd..0000000 --- a/.github/agents/ef-core/SKILL.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -name: ef-core -description: Entity Framework Core patterns for .NET projects using Pomelo MariaDB provider — migration workflow, startup auto-migrate, primary constructors, service layer structure, and the Linux LINQ ReadOnlySpan overload bug. Use when adding migrations, setting up DbContext, structuring services, writing LINQ queries, or troubleshooting EF Core issues. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: .NET 10 + EF Core + Pomelo.EntityFrameworkCore.MySql + MariaDB. ---- - -# EF Core Agent - -Guides correct Entity Framework Core usage with the Pomelo MariaDB provider. - ---- - -## Provider Setup - -``` -NuGet packages: - Microsoft.EntityFrameworkCore - Pomelo.EntityFrameworkCore.MySql - Microsoft.EntityFrameworkCore.Design ← local dev only; exclude from runtime image -``` - -```json -// appsettings.json -{ - "ConnectionStrings": { - "DefaultConnection": "Server=localhost;Port=3306;Database=_dev;User=;Password=;AllowPublicKeyRetrieval=true;" - } -} -``` - -```csharp -// Program.cs -builder.Services.AddDbContext(options => - options.UseMySql( - builder.Configuration.GetConnectionString("DefaultConnection"), - ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection")) - ) -); -``` - ---- - -## Entity Conventions - -```csharp -public class MyEntity -{ - public Guid Id { get; set; } - public string Name { get; set; } = string.Empty; - public DateTime CreatedAt { get; set; } - public DateTime? UpdatedAt { get; set; } -} -``` - -### DbContext (primary constructor) - -```csharp -public class ApplicationDbContext(DbContextOptions options) - : DbContext(options) -{ - public DbSet MyEntities => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - } -} // end ApplicationDbContext -``` - ---- - -## Migration Workflow - -```bash -# Add -dotnet ef migrations add --project src/.Api --startup-project src/.Api - -# Apply -dotnet ef database update --project src/.Api --startup-project src/.Api - -# List status -dotnet ef migrations list --project src/.Api - -# Rollback to migration -dotnet ef database update --project src/.Api - -# Remove last un-applied file (does not touch DB) -dotnet ef migrations remove --project src/.Api - -# Wipe schema — dangerous -dotnet ef database update 0 --project src/.Api -``` - -### Naming Conventions - -| Scenario | Name | -|----------|------| -| Initial schema | `InitialCreate` | -| Add table | `AddTable` | -| Add column | `AddTo` | -| Remove column | `RemoveFrom
` | -| Add index | `AddIndexTo
` | -| Refactor | `Refactor` | -| Seed change | `SeedData` | - ---- - -## Startup Auto-Migrate - -**ALWAYS `db.Database.Migrate()`. NEVER `EnsureCreated()`.** - -`EnsureCreated()` creates the schema without the `__EFMigrationsHistory` table — -permanently blocking future migrations on that database. - -```csharp -// Program.cs — after app build, before app.Run() -using (var scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} -``` - ---- - -## Linux LINQ ReadOnlySpan Gotcha - -**Silent bug on Linux that does NOT appear on macOS development.** - -```csharp -// ❌ BROKEN on Linux — compiles, throws at EF Core runtime -var results = db.Entities - .Where(e => new[] { "active", "pending" }.Contains(e.Status)) - .ToList(); - -// ✅ CORRECT — declare as List explicitly -var statuses = new List { "active", "pending" }; -var results = db.Entities - .Where(e => statuses.Contains(e.Status)) - .ToList(); -``` - -**Rule:** Always use `List` (not `string[]`) for collections used in -LINQ `.Contains()` against EF Core queries. - ---- - -## Service Layer Pattern - -Controllers are thin — all logic in scoped services behind interfaces. - -```csharp -// IMyService.cs -public interface IMyService -{ - Task GetAllAsync(); - Task GetByIdAsync(Guid id); // throws NotFoundException - Task CreateAsync(CreateRequest r); - Task UpdateAsync(Guid id, UpdateRequest r); - Task DeleteAsync(Guid id); -} - -// MyService.cs -public class MyService(ApplicationDbContext db) : IMyService -{ - // ── QUERY ───────────────────────────────────────────────────────────── - - // returns all entities sorted by name - public async Task GetAllAsync() => - await db.MyEntities - .OrderBy(e => e.Name) - .Select(e => new MyDto { Id = e.Id, Name = e.Name }) - .ToArrayAsync(); - - // throws NotFoundException if not found - public async Task GetByIdAsync(Guid id) => - await db.MyEntities - .Where(e => e.Id == id) - .Select(e => new MyDto { Id = e.Id, Name = e.Name }) - .FirstOrDefaultAsync() - ?? throw new NotFoundException($"MyEntity {id} not found"); -} // end MyService - -// Program.cs registration -builder.Services.AddScoped(); -``` - -### Domain Exceptions (→ RFC 7807 ProblemDetails) - -| Exception | HTTP Status | -|-----------|------------| -| `NotFoundException` | 404 | -| `ForbiddenException` | 403 | -| `BadRequestException` | 400 | -| `UnauthorizedException` | 401 | - ---- - -## Testing - -```csharp -// In-memory DB for unit tests (not real MariaDB) -services.AddDbContext(o => - o.UseInMemoryDatabase("TestDb_" + Guid.NewGuid())); -``` - -Never test against real MariaDB from CI unit tests — use in-memory for portability. - ---- - -## EF_KNOWLEDGE - -> Append new EF Core discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: [HelloNetworkWorld] Pomelo TCP connection on macOS Homebrew MariaDB — socket path auth not needed with password-authenticated user. Connection uses `Server=localhost;Port=3306;`. -- 2026-02-27: [HelloNetworkWorld] `db.Database.Migrate()` in Program.cs — InitialCreate migration applied cleanly on `hnw_dev` DB on first run. -- 2026-02-27: [DSC-modernization] Quartz.NET background job scheduler requires its own `AddQuartz()` DI extension. Keep Quartz config separate from EF Core registration to avoid Program.cs bloat. diff --git a/.github/agents/git-conventions/SKILL.md b/.github/agents/git-conventions/SKILL.md deleted file mode 100644 index a5e14d6..0000000 --- a/.github/agents/git-conventions/SKILL.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: git-conventions -description: Enforces branch naming, commit message format, and conventional commit types across all rloisell/ and bcgov-c/ repositories. Use when creating branches, writing commit messages, reviewing PR titles, or answering questions about git workflow conventions. -metadata: - author: Ryan Loiselle - version: "1.0" ---- - -# Git Conventions - -Shared skill — referenced by `github-workflow`, `session-workflow`, and any agent -that creates branches, commits, or PRs. - -## Branch Naming - -``` -/ - -feat/network-test-crud -fix/vite-build-target -chore/diagram-folder-structure -docs/deployment-analysis -refactor/service-layer-split -test/network-test-integration -``` - -| Type | Use for | -|------|---------| -| `feat` | New user-facing feature | -| `fix` | Bug fix | -| `chore` | Maintenance, tooling, config, dependency updates | -| `docs` | Documentation only | -| `refactor` | Restructuring without behaviour change | -| `test` | Test additions or corrections | - -Slugs: lowercase, hyphen-separated, ≤ 40 characters. - ---- - -## Commit Message Format - -``` -: - -- detail line 1 (what changed and why) -- detail line 2 -``` - -Subject line rules: -- Imperative mood: "add", "fix", "remove" — NOT "added", "fixes", "removing" -- No period at end -- ≤ 72 characters - -Body rules (optional, include when non-obvious): -- Blank line between subject and body -- Each line starts with `- ` -- Explain *why*, not just *what* - -### Examples - -``` -feat: add NetworkTest CRUD endpoints - -- Created NetworkTest entity with Pomelo MariaDB mapping -- Added CreateNetworkTestRequest / UpdateNetworkTestRequest DTOs -- Registered INetworkTestService in DI -``` - -``` -fix: correct Vite build target for top-level await - -- Changed build.target from default (es2020) to 'esnext' -- Default target does not support top-level await in main.jsx -``` - -``` -chore: add template Copilot agent skills - -- Added 8 SKILL.md agent skill directories -- Extracted 4 shared skills to agents/skills/ -- Created agent-evolution self-learning agent -``` diff --git a/.github/agents/github-workflow/SKILL.md b/.github/agents/github-workflow/SKILL.md deleted file mode 100644 index b4b98ee..0000000 --- a/.github/agents/github-workflow/SKILL.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -name: github-workflow -description: Manages the complete branch-first, PR-required GitHub workflow — branch naming, commit conventions, PR lifecycle, CI status monitoring, merge strategy, and GitHub Ruleset management. Use when creating branches, opening PRs, diagnosing CI failures, managing branch protection rules, or reviewing Dependabot PRs. -metadata: - author: Ryan Loiselle - version: "1.0" ---- - -# GitHub Workflow Agent - -Guides and executes the branch-first, PR-required workflow for all projects. -Knows branch protection rules, required status checks, and `gh` CLI PR lifecycle. - -For branch naming and commit format, see -[`../git-conventions/SKILL.md`](../git-conventions/SKILL.md). - ---- - -## Branch Protection Model - -All `main` branches use GitHub **Rulesets** (not classic branch protection — -rulesets work on public repos under the free plan). - -### Standard Rules -- No deletion or force-push to `main` -- PRs required before merging -- All registered status checks must pass before merge - -### Known Rulesets - -| Repo | Ruleset ID | Required Checks | -|------|-----------|-----------------| -| rloisell/HelloNetworkWorld | 13316979 | `.NET Build & Test`, `Frontend Build & Test` | - -> Add new repos here as rulesets are created. - ---- - -## Standard PR Lifecycle - -```bash -# 1. Create feature branch -git checkout -b feat/my-feature - -# 2. Commit -git add -A -git commit -m "feat: add my feature - -- added X -- updated Y" - -# 3. Push and open PR -git push origin feat/my-feature -gh pr create \ - --title "feat: add my feature" \ - --body "## Summary -- Added X -- Updated Y" \ - --base main - -# 4. Monitor CI -gh pr checks --watch - -# 5. Merge when all checks pass -gh pr merge --squash --delete-branch -git checkout main && git pull origin main -``` - ---- - -## Diagnosing CI Failures - -```bash -# Get most recent run ID -gh run list --workflow build-and-test.yml --limit 1 --json databaseId --jq '.[0].databaseId' - -# View failed steps -gh run view --log-failed 2>&1 | tail -50 -``` - -### Common Failure Patterns - -| Symptom | Cause | Fix | -|---------|-------|-----| -| `Top-level await is not available` | Vite build target too old | `build.target: 'esnext'` in vite.config.js | -| Status check required but never posted | Job `name:` ≠ ruleset context | Match `name:` in workflow YAML to ruleset string exactly | -| `npm ci` fails | `package-lock.json` not committed | Run `npm install` locally and commit lock file | -| `dotnet test` fails | Missing test DB or unapplied migration | Use in-memory DB in test setup | -| PR blocked, no checks posted | Workflow `paths:` filter excludes changed files | Broaden `paths:` to include changed file types | - ---- - -## Ruleset Management (gh CLI) - -```bash -# Create ruleset -gh api repos///rulesets --method POST --input ruleset.json - -# List rulesets -gh api repos///rulesets | jq '.[] | {id, name, enforcement}' - -# Update ruleset -gh api repos///rulesets/ --method PUT --input updated.json -``` - -### Status Check Context Names - -The `name:` field in each workflow job must **exactly match** the ruleset context string. -Job ID key is irrelevant — only `name:` matters. - -| Workflow | Job ID | `name:` field = required context | -|----------|--------|----------------------------------| -| `build-and-test.yml` | `test-api` | `.NET Build & Test` | -| `build-and-test.yml` | `frontend-build` | `Frontend Build & Test` | - ---- - -## Repo Visibility (rulesets require public or Pro) - -```bash -# Check visibility -gh repo view / --json visibility - -# Make public (rulesets on free plan require public repo) -gh repo edit / --visibility public -``` - ---- - -## Dependabot PR Review Strategy - -1. GitHub Actions version bumps — lowest risk; merge first -2. NuGet minor/patch — review changelog; merge after CI passes -3. npm minor/patch — `npm audit` locally; merge after CI passes -4. Major version bumps (React, Vite, ASP.NET Core) — manual test required - ---- - -## WORKFLOW_KNOWLEDGE - -> Append new workflow discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: Ruleset status check context strings must exactly match the `name:` field in the job, not the job ID key. -- 2026-02-27: `gh pr merge --squash --delete-branch` preferred — keeps main history clean and auto-removes PR branch. -- 2026-02-27: GitHub Rulesets on free plan require repo to be public — private repos silently ignore rulesets. -- 2026-02-28: Workflow `paths:` filter that only includes `src/**` blocks PRs touching `.github/` from ever satisfying required status checks. Broaden to `.github/**` when agent/config files should trigger CI. diff --git a/.github/agents/local-dev/SKILL.md b/.github/agents/local-dev/SKILL.md deleted file mode 100644 index 2378401..0000000 --- a/.github/agents/local-dev/SKILL.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -name: local-dev -description: Sets up, runs, and troubleshoots the local development environment — podman-compose multi-container stack, EF Core migrations, MariaDB socket and TCP connection patterns, port conventions, admin token seeding, and test execution. Use when setting up local dev, running migrations, debugging connection errors, or troubleshooting the local stack. -metadata: - author: Ryan Loiselle - version: "1.0" ---- - -# Local Development Agent - -Helps set up, run, and troubleshoot the local development environment. - ---- - -## Standard Port Conventions - -| Service | Local Port | URL | -|---------|-----------|-----| -| .NET API (template default) | 5005 | `http://localhost:5005` | -| React/Vite dev server | 5173 | `http://localhost:5173` | -| MariaDB | 3306 | TCP or socket | -| podman-compose API | 8080 | `http://localhost:8080` | - -> Check `src/.Api/Properties/launchSettings.json` for the actual API port. -> The CORS policy in `appsettings.Development.json` must include the Vite port. - ---- - -## Running Native (without podman-compose) - -```bash -# API -cd src/.Api -dotnet run # or: dotnet watch run (file-change reload) - -# Frontend -cd src/.WebClient -npm run dev # Vite proxies /api/* to the API port - -# MariaDB (macOS Homebrew) -brew services start mariadb -``` - ---- - -## Running With podman-compose - -```bash -podman-compose up --build # build and start all services -podman-compose up # start without rebuild -podman-compose down # stop, keep volumes -podman-compose down -v # stop and wipe volumes (clean slate) -podman-compose logs -f api # tail API logs -``` - -`podman-compose.yml` lives in `containerization/`. Mirrors OpenShift layout: -API on 8080, MariaDB on 3306, healthchecks on all services. - ---- - -## Environment Configuration - -```bash -cp src/.Api/appsettings.Development.json.example \ - src/.Api/appsettings.Development.json -``` - -`.Development.json` is in `.gitignore` — never commit it. - -```json -{ - "ConnectionStrings": { - "DefaultConnection": "Server=localhost;Port=3306;Database=_dev;User=;Password=;" - }, - "Authentication": { "BypassAuth": true }, - "Admin": { "Token": "local-admin-token" } -} -``` - -### MariaDB Socket Auth (macOS Homebrew) - -Homebrew MariaDB defaults to socket auth for root. Create a password-auth dev user: - -```sql --- Run as root: mariadb --skip-ssl -CREATE DATABASE _dev CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER ''@'localhost' IDENTIFIED BY ''; -GRANT ALL PRIVILEGES ON _dev.* TO ''@'localhost'; -FLUSH PRIVILEGES; -``` - -Use `Server=localhost;Port=3306;` (TCP) in the connection string with this user. - ---- - -## EF Core Migrations - -```bash -# Add migration -dotnet ef migrations add \ - --project src/.Api --startup-project src/.Api - -# Apply -dotnet ef database update \ - --project src/.Api --startup-project src/.Api - -# List status -dotnet ef migrations list --project src/.Api - -# Rollback -dotnet ef database update --project src/.Api - -# Remove last un-applied migration file -dotnet ef migrations remove --project src/.Api - -# Wipe schema (dangerous) -dotnet ef database update 0 --project src/.Api -``` - -### Naming Conventions - -| Scenario | Migration Name | -|----------|---------------| -| Initial schema | `InitialCreate` | -| Add table | `AddTable` | -| Add column | `AddTo
` | -| Remove column | `RemoveFrom
` | -| Refactor | `Refactor` | - -### Startup Auto-Migrate - -```csharp -// Program.cs — always Migrate(), never EnsureCreated() -using (var scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} -``` - ---- - -## Running Tests - -```bash -dotnet test # all tests -dotnet test tests/.Tests/ # specific project -dotnet test --verbosity normal # with output -cd src/.WebClient && npm test # Vitest frontend tests -``` - ---- - -## Common Issues - -| Symptom | Cause | Fix | -|---------|-------|-----| -| `Auth plugin 'caching_sha2_password' not supported` | Wrong MariaDB auth plugin | Add `AllowPublicKeyRetrieval=true;` or recreate user with `mysql_native_password` | -| `Unable to connect to database` | MariaDB not running | `brew services start mariadb` or `podman-compose up db` | -| `Migration has already been applied` | DB ahead of code | Reset: `dotnet ef database update 0` then `update` | -| CORS error in browser | Vite port not in allowlist | Add `http://localhost:` to `Cors:AllowedOrigins` | -| `EACCES` on `npm run dev` | Port 5173 in use | `lsof -ti:5173 \| xargs kill` | -| `dotnet run` fails — port bound | API port in use | `lsof -ti:5005 \| xargs kill` | -| `Model backing the context has changed` | Unapplied migration | `dotnet ef database update` | - ---- - -## LOCAL_DEV_KNOWLEDGE - -> Append new local dev discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: [HelloNetworkWorld] macOS Homebrew MariaDB uses socket auth for root. Created dedicated `hnw_dev` DB with password-auth user. Connection string: `Server=localhost;Port=3306;Database=hnw_dev;User=hnw_user;Password=...;` -- 2026-02-27: [HelloNetworkWorld] API on port 5200, Vite on 5175 (non-default). After EF Core InitialCreate migration, full CRUD verified via curl. Quartz.NET background scheduler starts on API boot. diff --git a/.github/agents/observability/SKILL.md b/.github/agents/observability/SKILL.md deleted file mode 100644 index b0def26..0000000 --- a/.github/agents/observability/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: observability -description: Structured logging, metrics, and tracing for BC Gov .NET / React / OpenShift projects — Serilog JSON configuration, log level standards, PII-free logging rules, Prometheus pod annotations, OpenTelemetry .NET SDK setup, health check endpoints, and alert baseline. Use when configuring logging, adding metrics instrumentation, setting up health checks, or diagnosing production issues. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: .NET 10 / ASP.NET Core. Serilog 3+. OpenTelemetry .NET SDK 1.x. Emerald OpenShift — Prometheus pull model. ---- - -# Observability Agent - -Standardises logging, metrics, tracing, and health checks across BC Gov projects. - ---- - -## Logging — Serilog - -### Package install -```bash -dotnet add package Serilog.AspNetCore -dotnet add package Serilog.Formatting.Compact -dotnet add package Serilog.Sinks.Console -``` - -### Program.cs bootstrap -```csharp -// LOGGING — structured JSON to stdout (picked up by OpenShift log aggregation) -Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(builder.Configuration) - .Enrich.FromLogContext() - .Enrich.WithMachineName() - .Enrich.WithEnvironmentName() - .WriteTo.Console(new CompactJsonFormatter()) - .CreateLogger(); - -builder.Host.UseSerilog(); -``` - -### appsettings.json log levels -```json -{ - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Warning", - "System": "Warning" - } - } - } -} -``` - -> Log level `Warning` for EF Core SQL commands — never log SQL at `Information` in production -> (may expose query parameters containing PII). - -### Log level standards - -| Level | Use | -|-------|-----| -| `Verbose` | Development only — detailed flow tracing | -| `Debug` | Diagnostic context helpful in test environments | -| `Information` | Normal application events (startup, user actions, state transitions) | -| `Warning` | Recoverable abnormal conditions (retry, missing optional config) | -| `Error` | Exceptions / failures that require attention | -| `Fatal` | Unrecoverable startup / crash events | - -### PII-free logging rules - -**NEVER** log: -- User names, email addresses -- SIN, DL number, student number, or other personal identifiers -- Connection strings, tokens, passwords, or Vault values -- Full HTTP request/response bodies (may contain form data) -- Query string parameters that may carry tokens (`?code=`, `?token=`) - -**Safe to log:** -- User ID (GUID / sub claim) — opaque identifier only -- HTTP method + path (no query string) -- HTTP status code -- Duration (ms) -- Correlation ID / trace ID - -### Structured logging pattern -```csharp -// Use structured properties, not string interpolation -_logger.LogInformation("Created work item {WorkItemId} for project {ProjectId}", - item.Id, item.ProjectId); - -// NOT: -_logger.LogInformation($"Created work item {item.Id}"); -``` - ---- - -## Health Checks - -### Package -```bash -dotnet add package AspNetCore.HealthChecks.MySql -``` - -### Registration in Program.cs -```csharp -// HEALTH CHECKS — /api/health (liveness) and /api/health/details (readiness) -builder.Services.AddHealthChecks() - .AddMySql( - connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!, - name: "database", - tags: ["ready"]); - -// ... -app.MapHealthChecks("/api/health"); -app.MapHealthChecks("/api/health/details", new HealthCheckOptions -{ - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, - Predicate = _ => true, -}); -``` - -### Containerfile health check -```dockerfile -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget -qO- http://localhost:8080/api/health || exit 1 -``` - ---- - -## Metrics — Prometheus - -Emerald uses a Prometheus pull model. Annotate pods so Prometheus discovers the metrics endpoint. - -### Pod annotations (Helm values.yaml) -```yaml -podAnnotations: - prometheus.io/scrape: "true" - prometheus.io/path: "/metrics" - prometheus.io/port: "8080" -``` - -### .NET metrics endpoint (ASP.NET Core) -```bash -dotnet add package prometheus-net.AspNetCore -``` -```csharp -// METRICS — expose /metrics for Prometheus scrape -app.UseHttpMetrics(); // request duration, count, in-flight -app.MapMetrics(); // GET /metrics -``` - -### Custom metrics example -```csharp -// create once at class level (static) — Prometheus counters are global -private static readonly Counter _itemsCreated = - Metrics.CreateCounter("app_items_created_total", "Number of work items created.", - labelNames: ["project_id"]); - -// in service method — increment counter when an item is created -_itemsCreated.WithLabels(projectId.ToString()).Inc(); -``` - ---- - -## Tracing — OpenTelemetry - -### Packages -```bash -dotnet add package OpenTelemetry.Extensions.Hosting -dotnet add package OpenTelemetry.Instrumentation.AspNetCore -dotnet add package OpenTelemetry.Instrumentation.Http -dotnet add package OpenTelemetry.Exporter.Console # dev only -``` - -### Registration -```csharp -// TRACING — OpenTelemetry with OTLP export (or console in dev) -builder.Services.AddOpenTelemetry() - .WithTracing(tracing => - { - tracing - .SetResourceBuilder(ResourceBuilder.CreateDefault() - .AddService(serviceName: "my-app-api")) - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation(); - - if (builder.Environment.IsDevelopment()) - tracing.AddConsoleExporter(); - else - tracing.AddOtlpExporter(opt => - opt.Endpoint = new Uri(builder.Configuration["Otlp:Endpoint"]!)); - }); -``` - ---- - -## Alert Baseline (document in securityNextSteps.md) - -| Signal | Threshold | Action | -|--------|-----------|--------| -| HTTP 5xx rate | > 5% of requests over 5 min | Alert on-call | -| DB health check failing | > 2 consecutive failures | Alert on-call | -| Pod restart count | > 3 in 10 min | Alert on-call | -| High memory / CPU | > 90% of request limit for 5 min | Alert + scale | -| Auth failures (401/403) | Spike > 10× baseline | Security alert | - ---- - -## OBSERVABILITY_KNOWLEDGE - -```yaml -confirmed_facts: - - "CompactJsonFormatter writes single-line JSON to stdout — required for OpenShift log aggregation" - - "Prometheus on Emerald uses pod annotations to discover /metrics endpoints" - - "EF Core SQL logging at Information level may expose query parameters — use Warning" - - "OpenTelemetry AddEntityFrameworkCoreInstrumentation requires EFCore instrumentation package" - - "Health check at /api/health is used by OpenShift liveness probe; /api/health/details for readiness" -common_pitfalls: - - "Never log PII — user emails, names, or identifying numbers in structured properties" - - "Metrics.CreateCounter must be static — creating per-request instances causes memory leaks" - - "prometheus-net endpoint conflicts with ASP.NET Core minimal API route if MapMetrics() called after MapControllers()" -``` diff --git a/.github/agents/security-architect/SKILL.md b/.github/agents/security-architect/SKILL.md deleted file mode 100644 index e9c3be1..0000000 --- a/.github/agents/security-architect/SKILL.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -name: security-architect -description: Security architecture and analysis for BC Gov .NET / React / OpenShift projects — OWASP Top 10 mitigations, SAST/DAST toolchain, secrets management with Vault, container security baseline, input validation, audit logging, OIDC session management, STRA/PIA process, and security hardening roadmap. Use when designing security controls, reviewing code for vulnerabilities, setting up scanning workflows, or completing the AI/securityNextSteps.md hardening plan. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: .NET 10 / ASP.NET Core. React / Vite. Emerald OpenShift 4.x. BC Gov STRA/PIA process. References vault-secrets shared skill. ---- - -# Security Architect Agent - -Drives security design, threat modelling, and hardening activities for BC Government projects. - -**Shared skills referenced by this agent:** -- Vault path conventions, ESO CRD, CI secrets → [`../vault-secrets/SKILL.md`](../vault-secrets/SKILL.md) -- BC Gov platform constraints → [`../bc-gov-emerald/SKILL.md`](../bc-gov-emerald/SKILL.md) - ---- - -## OWASP Top 10 — .NET 10 / React Controls - -| # | Risk | .NET Control | React Control | -|---|------|-------------|---------------| -| A01 | Broken Access Control | `[Authorize]`, `ForbiddenException` service guard, role claims | Route guards in React Router; API response drives visibility | -| A02 | Cryptographic Failures | `PasswordHasher` only; never store plain text; TLS edge on Route | HTTPS enforced at OpenShift Route; no secrets in JS bundle | -| A03 | Injection | EF Core parameterised queries only; prohibit `FromSqlRaw`; validate with data annotations | No `dangerouslySetInnerHTML`; parameterise API calls | -| A04 | Insecure Design | STRA before first environment; threat model per epic | Spec-kitty workflow captures assumptions; security review step | -| A05 | Security Misconfiguration | Remove default endpoints; CORS named policies only (`ProdCors` explicit origins) | CSP headers via nginx `add_header`; no wildcard origins | -| A06 | Vulnerable Components | Dependabot + `dependency-review.yml` + Trivy FS scan in CI | Same; `package-lock.json` committed | -| A07 | Auth Failures | OIDC PKCE only (no implicit); short-lived access tokens; silent refresh via refresh token rotation | `oidc-client-ts`; `onSigninCallback` normalises URL; backchannel logout endpoint | -| A08 | Data Integrity | Cosign image signing (future); Helm chart values pinned to SHA tags in prod | `integrity` attribute on CDN assets | -| A09 | Logging/Monitoring | Structured Serilog + no PII in logs; EF Core interceptor for change audit | No sensitive data in browser console | -| A10 | SSRF | Prohibit user-controlled URLs in `HttpClient`; allowlist outbound hosts | API proxies all external calls; no direct browser-to-third-party sensitive calls | - ---- - -## Input Validation (.NET 10) - -### Data Annotations (required on all incoming DTOs) -```csharp -public record CreateItemRequest( - [Required, StringLength(200)] string Name, - [Required, Range(1, int.MaxValue)] int ProjectId, - [EmailAddress] string? ContactEmail -); -``` - -### Model-state gate in controller -```csharp -// POST /api/items — create a new work item -[HttpPost] -public async Task Create([FromBody] CreateItemRequest request) -{ - if (!ModelState.IsValid) return BadRequest(ModelState); - var result = await _service.CreateAsync(request); - return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); -} -``` - -### SQL injection prevention -- **NEVER use `FromSqlRaw`** — use EF Core LINQ operators exclusively -- If raw SQL is unavoidable: use `FromSqlInterpolated` with `$`-string params -- Log a warning and add to `securityNextSteps.md` review column whenever raw SQL is introduced - -### Output encoding -- Razor: `@Html.Encode()` / default encoding is sufficient -- React: JSX auto-escapes; never use `dangerouslySetInnerHTML` -- JSON responses: `System.Text.Json` default serialiser; no custom formatters - ---- - -## Audit Logging - -### Purpose -Every state-changing action on data classified Medium or above must produce a tamper-evident audit record: **who** did **what** to **which record** at **when**. - -### EF Core SaveChanges interceptor -```csharp -// AUDIT INTERCEPTOR — captures Insert/Update/Delete to audit table -public class AuditInterceptor : SaveChangesInterceptor -{ - public override InterceptionResult SavingChanges( - DbContextEventData eventData, - InterceptionResult result) - { - var entries = eventData.Context!.ChangeTracker.Entries() - .Where(e => e.State is EntityState.Added - or EntityState.Modified - or EntityState.Deleted); - foreach (var entry in entries) - { - var auditEntry = new AuditLog - { - TableName = entry.Metadata.GetTableName()!, - Action = entry.State.ToString(), - RecordId = entry.Properties - .FirstOrDefault(p => p.Metadata.IsPrimaryKey()) - ?.CurrentValue?.ToString(), - ChangedBy = _httpContextAccessor.HttpContext? - .User.FindFirstValue(ClaimTypes.NameIdentifier), - ChangedAt = DateTime.UtcNow, - OldValues = entry.State == EntityState.Modified - ? JsonSerializer.Serialize( - entry.OriginalValues.ToObject()) - : null, - NewValues = entry.State != EntityState.Deleted - ? JsonSerializer.Serialize( - entry.CurrentValues.ToObject()) - : null, - }; - eventData.Context.Set().Add(auditEntry); - } - return result; - } -} -``` - -Register in `Program.cs`: -```csharp -builder.Services.AddDbContext(opt => - opt.UseMySql(connectionString, serverVersion) - .AddInterceptors(new AuditInterceptor(sp.GetRequiredService()))); -``` - ---- - -## Container Security Baseline - -All Containerfiles **must** satisfy: - -| Control | Implementation | -|---------|---------------| -| Non-root user | `RUN addgroup -S appgroup && adduser -S appuser -G appgroup` → `USER appuser` | -| Drop all capabilities | `cap_drop: [ALL]` in compose / Pod `securityContext.capabilities.drop: [ALL]` | -| No privilege escalation | `securityContext.allowPrivilegeEscalation: false` | -| Read-only root FS | `readOnlyRootFilesystem: true`; mount writable `/tmp` as emptyDir | -| Port 8080 only | `EXPOSE 8080`; `ASPNETCORE_URLS=http://+:8080` | -| Pinned base images | Use SHA digest in prod Containerfiles: `FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:` | - -OpenShift Pod SecurityContext (apply in Helm `deployment.yaml`): -```yaml -securityContext: - runAsNonRoot: true - runAsUser: 1001 - fsGroup: 1001 - seccompProfile: - type: RuntimeDefault - capabilities: - drop: [ALL] - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true -``` - ---- - -## SAST / DAST Toolchain - -### SAST (Static Analysis) -| Tool | Workflow | Gate | -|------|----------|------| -| CodeQL | `.github/workflows/codeql.yml` | Blocks PR on CRITICAL/HIGH (requires GHAS) | -| Trivy FS | `.github/workflows/trivy-scan.yml` (fs job) | Blocks PR on CRITICAL/HIGH CVE | -| Trivy Image | `.github/workflows/trivy-scan.yml` (image job) | Scans post-push image on develop | -| Dependency Review | `.github/workflows/dependency-review.yml` | Blocks PR if new CRITICAL/HIGH dependency introduced | -| Gitleaks | `.github/workflows/secrets-scan.yml` | Blocks push/PR on detected secrets | - -### DAST (Dynamic Analysis — future roadmap) -- OWASP ZAP baseline scan against dev environment on schedule -- Document findings in `AI/securityNextSteps.md` DAST column -- Recommended: ZAP GitHub Action with active scan profile - ---- - -## Authentication Session Management (OIDC PKCE) - -### Token lifecycle -1. User redirected to DIAM/Keycloak with PKCE challenge (`code_challenge`, `code_challenge_method=S256`) -2. Auth code returned — exchanged for `access_token` (short-lived) + `refresh_token` -3. `oidc-client-ts` (`UserManager`) handles silent refresh automatically via `automaticSilentRenew: true` -4. On token expiry: silent iframe renew or redirect back to IdP -5. Logout: call `/end_session_endpoint` + clear local session; implement backchannel logout endpoint at `/logout/backchannel` in API - -### React `oidc-client-ts` configuration -```js -// src/auth/authConfig.js — OIDC UserManager config for DIAM -export const userManagerConfig = { - authority: process.env.VITE_OIDC_AUTHORITY, - client_id: process.env.VITE_OIDC_CLIENT_ID, - redirect_uri: `${window.location.origin}/callback`, - post_logout_redirect_uri: `${window.location.origin}/`, - response_type: 'code', // PKCE - scope: 'openid profile email', - automaticSilentRenew: true, - silent_redirect_uri: `${window.location.origin}/silent-callback.html`, - filterProtocolClaims: true, - loadUserInfo: true, -}; -``` - -### API backchannel logout endpoint -```csharp -// POST /logout/backchannel — OIDC backchannel logout handler -[HttpPost("/logout/backchannel")] -[AllowAnonymous] -public IActionResult BackchannelLogout([FromForm] string logout_token) -{ - // Validate logout_token JWT (issuer, audience, sub/sid claims) - // Invalidate server-side session or token cache for that sub/sid - return Ok(); -} -``` - ---- - -## STRA / PIA Process (BC Gov) - -### When to trigger a STRA -- New service storing personal information → **STRA required before TEST environment** -- DataClass: Medium or above → STRA mandatory -- External API integration involving personal data → STRA required -- New OAuth client registered in DIAM → STRA review section required - -### STRA checklist -- [ ] Identify data elements stored / processed (DataClass: Low / Medium / High / Critical) -- [ ] Complete [STRA template](https://intranet.gov.bc.ca/iit/products-services/information-security/security-threat-and-risk-assessments) with Ministry ISSO -- [ ] Capture accepted risks in `AI/securityNextSteps.md` → "STRA Status" row -- [ ] Re-assess if architecture changes materially (new DB, new integration, new data type) - -### When to trigger a PIA -- Service collects, uses, or discloses personal information about BC residents -- New automated decision supported by personal data -- Third-party data processor involved (cloud provider, vendor) - ---- - -## SECURITY_KNOWLEDGE - -```yaml -confirmed_facts: - - "DataClass: Medium required on Emerald — confirmed 2026-02-23 (Low has no VIP)" - - "BC Gov STRA templates maintained by OCIO; Ministry ISSO is the reviewer" - - "DIAM uses Keycloak; standard realm is 'standard' on loginproxy.gov.bc.ca" - - "CodeQL on bcgov-c org requires GitHub Advanced Security (GHAS) enabled on repo" - - "Gitleaks detects secrets in git history across all branches on push" - - "Trivy: exit-code 1 on CRITICAL/HIGH blocks the build step" - - "Vault Agent Injector and External Secrets Operator both supported on Emerald" - - "AllowPrivilegeEscalation: false enforced by Emerald cluster admission webhook" -common_pitfalls: - - "FromSqlRaw does NOT parameterise — use FromSqlInterpolated for any raw SQL" - - "Silent renew iframe fails if third-party cookies are blocked (Safari ITP) — use refresh_token grant instead" - - "AuditInterceptor must be registered before DbContext is first resolved — register in AddDbContext, not AddSingleton" - - "Vault Agent Injector annotations must be on the Pod template spec, not the Deployment metadata" -``` diff --git a/.github/agents/session-workflow/SKILL.md b/.github/agents/session-workflow/SKILL.md deleted file mode 100644 index 5c6c4a0..0000000 --- a/.github/agents/session-workflow/SKILL.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -name: session-workflow -description: Governs session startup and shutdown discipline for AI-assisted development — reads orientation files, checks git state, reviews Dependabot PRs, updates session logs, and commits work. Use at the start and end of every development session, or when asked about session process and AI collaboration protocol. -metadata: - author: Ryan Loiselle - version: "1.0" ---- - -# Session Workflow Agent - -Enforces consistent startup and shutdown discipline across all development sessions -regardless of project. Invoked at the start and end of every working session. - -For AI session file formats (WORKLOG, CHANGES, COMMANDS, COMMIT_INFO), see -[`../ai-session-files/SKILL.md`](../ai-session-files/SKILL.md). - -For branch naming and commit format, see -[`../git-conventions/SKILL.md`](../git-conventions/SKILL.md). - ---- - -## Session Startup Protocol - -Run silently at the start of every session. If a file is missing, note the gap and continue. - -### Step 1 — Orientation (read in order) - -| Priority | File | Purpose | -|----------|------|---------| -| 1 | `AI/nextSteps.md` | MASTER TODO — what is in progress | -| 2 | `CODING_STANDARDS.md` | Coding conventions, AI guardrails, project rules | -| 3 | `docs/deployment/EmeraldDeploymentAnalysis.md` | Platform deployment context (if it exists) | - -Open with **one sentence** summarising the current state from `AI/nextSteps.md`. -Example: "Session 5 continues from merge of PR #12; next item is EF Core migration -for the new NetworkTest entity." - -### Step 2 — Git State Check - -```bash -git status -git log --oneline -5 -``` - -Report uncommitted changes from a prior session. If stale changes exist, ask -Ryan whether to stash, commit, or discard before proceeding. - -### Step 3 — Dependabot PR Check - -```bash -gh pr list --state open --author app/dependabot --json number,title,createdAt -``` - -If new Dependabot PRs are not in `AI/nextSteps.md` Tier 4, add them. -Review strategy: GitHub Actions bumps → NuGet minor/patch → npm minor/patch → -major version bumps (require manual test). - -### Step 4 — Branch State - -If on `main` and there is in-progress work noted in `nextSteps.md`, create or -switch to the appropriate feature branch before making any changes. -`main` is always protected — no direct commits. - ---- - -## Session Shutdown Protocol - -Run at the end of every session before the conversation ends. - -### Step 1 — Update AI Session Files - -Update `AI/WORKLOG.md`, `AI/CHANGES.csv`, `AI/COMMANDS.sh`, `AI/COMMIT_INFO.txt` -per the format in [`../ai-session-files/SKILL.md`](../ai-session-files/SKILL.md). - -### Step 2 — Commit and Push - -```bash -git add -A -git commit -m "type: short description - -- detail 1 -- detail 2" - -# If on a feature branch: -git push origin - -# If no PR exists yet: -gh pr create --title "type: short description" --base main -``` - -Never commit directly to `main`. Use `github-workflow` agent for PR management. - -### Step 3 — Verify AI/nextSteps.md State - -- Mark completed rows `✅` with strikethrough -- Add any newly discovered tasks -- Confirm the "next session start" state is accurate - ---- - -## Process Rules - -1. **Never skip startup orientation** — AI has no memory between sessions. `nextSteps.md` IS the memory. -2. **Branch before code** — `main` is protected; create a feature branch before any file change. -3. **Log everything** — WORKLOG, CHANGES, COMMANDS, COMMIT_INFO are the audit trail. -4. **One session = one commit set** — Don't accumulate uncommitted work across sessions. -5. **Ask about stale changes** — Never silently overwrite or discard uncommitted work. -6. **End every session with a push** — Nothing should exist only on local disk at session end. - ---- - -## AI File Maintenance (quick reference) - -| File | When | Format | -|------|------|--------| -| `AI/nextSteps.md` | Start + end of session | See CODING_STANDARDS.md §AI/nextSteps | -| `AI/WORKLOG.md` | End of session | Dated markdown section | -| `AI/CHANGES.csv` | Every create/modify/delete | CSV: date, action, path, reason | -| `AI/COMMANDS.sh` | Every significant command | Shell with date comment | -| `AI/COMMIT_INFO.txt` | After every commit/push | One line per commit | - ---- - -## PROCESS_KNOWLEDGE - -> Append new session process discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: Main branch is protected via GitHub Ruleset (ID 13316979 on HelloNetworkWorld). All changes must go through PRs. Required checks: `.NET Build & Test`, `Frontend Build & Test`. -- 2026-02-27: Runtime config fetch in `main.jsx` uses top-level await — requires `build.target: 'esnext'` in vite.config.js. Default esbuild target (es2020) does not support top-level await. diff --git a/.github/agents/spec-kitty/SKILL.md b/.github/agents/spec-kitty/SKILL.md deleted file mode 100644 index 61ca771..0000000 --- a/.github/agents/spec-kitty/SKILL.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -name: spec-kitty -description: Drives spec-first feature development using the spec-kitty framework — enforces spec.md before implementation, creates WP task files with correct YAML frontmatter, runs validate-tasks, and tracks lanes through the planned→done lifecycle. Use when starting a new feature, writing spec or plan files, creating WP task files, or validating the spec-kitty task board. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: Requires spec-kitty Python package. Designed for projects following CODING_STANDARDS.md §11. ---- - -# Spec-Kitty Agent - -Enforces spec-first development: no implementation without a spec, no spec without -acceptance criteria, no code without WP task breakdown and validation. - -**This is the generic template version.** Project-specific feature status tables and -spec IDs live in the project's own `spec-kitty.agent.md` or `spec-kitty/SKILL.md`. - ---- - -## Core Rule - -**Write spec → plan → WP files BEFORE any implementation code.** - -``` -kitty-specs/ - {NNN}-{feature-slug}/ - spec.md ← requirements, user stories, success criteria - plan.md ← phased implementation plan - tasks/ - WP01-title.md ← atomic work package (< 2h each) - WP02-title.md - spec/ - fixtures/ - openapi/ ← OpenAPI request/response .json examples - db/ ← SQL migration preview .sql files -``` - ---- - -## Installation and Init - -```bash -pip install spec-kitty - -spec-kitty init --here --ai copilot --non-interactive --no-git --force - -git add .kittify/ .github/prompts/ -git commit -m "chore: initialise spec-kitty" -``` - ---- - -## Creating a Feature - -```bash -spec-kitty agent feature create-feature --id 001 --name "feature-slug" - -# After writing spec.md, plan.md, WP files: -spec-kitty validate-tasks --all -# Must be 0 mismatches before writing any code -``` - ---- - -## spec.md Required Sections - -```markdown -# Feature NNN — Feature Name - -**Author**: Ryan Loiselle — Developer / Architect -**AI tool**: GitHub Copilot — AI pair programmer / code generation -**Updated**: - -## Overview -## User Stories -## Requirements -### Functional Requirements -### Non-Functional Requirements -## Success Criteria -## Out of Scope -## Dependencies -## Notes -``` - ---- - -## plan.md Required Sections - -```markdown -# Plan — Feature NNN — Feature Name - -## Technical Approach -## Entity Changes -## API Endpoints -| Method | Path | Request Body | Response | Status | -## Frontend Changes -## Testing Approach -## Phases -- Phase 1 — Backend: entities, migration, service, controller -- Phase 2 — Frontend: queries, hooks, page components -- Phase 3 — Testing and polish -``` - ---- - -## WP Task File Format - -```yaml ---- -work_package_id: "WP01" -title: "Short description (imperative, < 10 words)" -lane: "planned" -subtasks: - - "WP01" -phase: "Phase 1 — Backend" -assignee: "" -agent: "" -shell_pid: "" -review_status: "" -reviewed_by: "" -history: - - timestamp: "YYYY-MM-DDTHH:MM:SSZ" - lane: "planned" - agent: "system" - action: "Created for feature spec" ---- - -## Work Package: WP01 — Title - -### Goal -### Deliverables -- [ ] `src/.Api/Entities/.cs` - -### Acceptance Criteria -- [ ] AC-01: - -### Notes -``` - -**Lane lifecycle:** `planned` → `in-progress` → `review` → `done` - -WPs must be atomic — completable in under 2 hours. Split larger work. - ---- - -## OpenAPI Fixtures - -``` -spec/fixtures/openapi/-request.json -spec/fixtures/openapi/-response.json -``` - ---- - -## Pre-Merge Checklist - -1. All WP files have `lane: "done"` -2. `spec-kitty validate-tasks --all` → 0 mismatches -3. All success criteria checked off in `spec.md` -4. OpenAPI fixtures committed for each endpoint -5. Unit tests passing - ---- - -## Code Reference Requirement - -In implementation files, add a method comment: -```csharp -// Implements: WP02 (003-network-test-config) -``` - ---- - -## Files to Commit - -```bash -git add .kittify/ kitty-specs/ .github/prompts/ -# .gitignore must include: __pycache__/ *.pyc *.pyo -``` - ---- - -## Linux LINQ Gotcha (relevant to spec Notes sections) - -```csharp -// ❌ BROKEN on Linux runtime — ReadOnlySpan.Contains() overload conflict -var results = db.Entities.Where(e => new[] { "a", "b" }.Contains(e.Name)).ToList(); - -// ✅ CORRECT -var names = new List { "a", "b" }; -var results = db.Entities.Where(e => names.Contains(e.Name)).ToList(); -``` - -Document this in any spec that involves `.Contains()` on string collections in LINQ. - ---- - -## SPEC_KNOWLEDGE - -> Append new spec-kitty discoveries here. -> Format: `YYYY-MM-DD: ` - -- 2026-02-27: [HelloNetworkWorld] spec-kitty init creates `.kittify/` and updates `.github/prompts/`. Both must be committed. Add `__pycache__/` to `.gitignore`. -- 2026-02-27: [HelloNetworkWorld] EF Core on Linux — `new[] { ... }.Contains()` → use `new List { ... }` explicitly. Add note to any spec involving LINQ Contains on string collections. diff --git a/.github/agents/vault-secrets/SKILL.md b/.github/agents/vault-secrets/SKILL.md deleted file mode 100644 index dbd27ad..0000000 --- a/.github/agents/vault-secrets/SKILL.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -name: vault-secrets -description: HashiCorp Vault secrets management on BC Gov Emerald OpenShift — Vault path conventions, External Secrets Operator CRD patterns, Vault Agent Injector annotations, GitHub Actions Vault integration for CI build-time secrets, and Helm shape-only Secret templates. Use when bootstrapping Vault for a new project, reading or writing secrets in CI, or configuring pod-level secret injection. -metadata: - author: Ryan Loiselle - version: "1.0" -compatibility: Emerald OpenShift 4.x. HashiCorp Vault. External Secrets Operator v0.9+. GitHub Actions. ---- - -# Vault Secrets Shared Skill - -Standardises secrets management across all BC Gov projects on Emerald. - -**Consumed by:** -- `../security-architect/SKILL.md` -- `../bc-gov-devops/SKILL.md` -- `../ci-cd-pipeline/SKILL.md` - ---- - -## Vault Path Convention - -``` -secret/// - -Examples: - secret/be808f/dev/db-password - secret/be808f/dev/connection-string - secret/be808f/prod/oidc-client-secret -``` - -Environment values: `dev`, `test`, `prod` -License plate: the 6-character code from Platform Product Registry (e.g., `be808f`) - ---- - -## GitHub Secrets Required (per app repo) - -| Secret Name | Value | -|-------------|-------| -| `ARTIFACTORY_USERNAME` | Artifactory service account name | -| `ARTIFACTORY_PASSWORD` | Artifactory service account token | -| `GITOPS_TOKEN` | PAT with write access to the GitOps repo | -| `VAULT_ADDR` | `https://vault.developer.gov.bc.ca` (if reading Vault in CI) | -| `VAULT_ROLE_ID` | AppRole role ID (if using AppRole auth) | -| `VAULT_SECRET_ID` | AppRole secret ID (if using AppRole auth) | - ---- - -## External Secrets Operator (ESO) — Recommended - -ESO is the preferred approach on Emerald. It runs in the cluster and syncs Vault secrets to -Kubernetes `Secret` resources automatically. - -### SecretStore CRD (one per namespace) -```yaml -# gitops/charts//templates/secretstore.yaml -apiVersion: external-secrets.io/v1beta1 -kind: SecretStore -metadata: - name: vault-secretstore - namespace: {{ .Values.namespace }} -spec: - provider: - vault: - server: "https://vault.developer.gov.bc.ca" - path: "secret" - version: "v2" - auth: - kubernetes: - mountPath: "kubernetes" - role: "{{ .Values.vaultRole }}" - serviceAccountRef: - name: "{{ include "app.serviceAccountName" . }}" -``` - -### ExternalSecret CRD (one per secret group) -```yaml -# gitops/charts//templates/externalsecret.yaml -apiVersion: external-secrets.io/v1beta1 -kind: ExternalSecret -metadata: - name: app-secrets - namespace: {{ .Values.namespace }} -spec: - refreshInterval: 10m - secretStoreRef: - name: vault-secretstore - kind: SecretStore - target: - name: app-secrets - creationPolicy: Owner - data: - - secretKey: db-password - remoteRef: - key: secret/{{ .Values.licenseplate }}/{{ .Values.env }}/db-password - - secretKey: connection-string - remoteRef: - key: secret/{{ .Values.licenseplate }}/{{ .Values.env }}/connection-string -``` - ---- - -## Vault Agent Injector — Alternative - -Use if ESO is not installed in the namespace: - -```yaml -# Annotations on the Pod template in deployment.yaml -metadata: - annotations: - vault.hashicorp.com/agent-inject: "true" - vault.hashicorp.com/role: "{{ .Values.vaultRole }}" - vault.hashicorp.com/agent-inject-secret-db-password: | - secret/data/{{ .Values.licenseplate }}/{{ .Values.env }}/db-password - vault.hashicorp.com/agent-inject-template-db-password: | - {{`{{- with secret "secret/data/`}}{{ .Values.licenseplate }}{{`/`}}{{ .Values.env }}{{`/db-password" }}`}} - {{`{{ .Data.data.value }}`}} - {{`{{- end }}`}} -``` - -> ⚠️ Vault Agent Injector annotations must be on the **Pod template `metadata`**, not the -> Deployment `metadata`. The injector mutates pods at admission time. - ---- - -## Helm Secret — Shape Template Only - -The Helm `secret.yaml` must never contain real values. Use as shape only: - -```yaml -# gitops/charts//templates/secret.yaml -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "app.fullname" . }}-secrets - namespace: {{ .Values.namespace }} -type: Opaque -data: {} -# Values are populated at runtime by External Secrets Operator or Vault Agent Injector. -# Never commit real secrets to this file or to values.yaml. -``` - ---- - -## GitHub Actions — Vault Integration (CI Build-time Secrets) - -```yaml -- name: Import secrets from Vault - uses: hashicorp/vault-action@v3 - with: - url: ${{ secrets.VAULT_ADDR }} - method: approle - roleId: ${{ secrets.VAULT_ROLE_ID }} - secretId: ${{ secrets.VAULT_SECRET_ID }} - secrets: | - secret/data/${{ env.LICENSE }}/dev/db-password | DB_PASSWORD ; - secret/data/${{ env.LICENSE }}/dev/connection-string | CONNECTION_STRING -``` - ---- - -## VAULT_KNOWLEDGE - -```yaml -confirmed_facts: - - "Vault Agent Injector annotations must be on Pod template metadata, not Deployment metadata" - - "ESO ExternalSecret refreshInterval of 10m is typical for non-critical secrets" - - "Vault AppRole auth requires VAULT_ROLE_ID and VAULT_SECRET_ID in GitHub Secrets" - - "Helm secret.yaml must be shape-only — never commit real values" - - "secret/data/// is KV v2 path (note: /data/ infix required for API calls)" -common_pitfalls: - - "KV v2 path in Vault CLI is secret//... but the API/ESO remoteRef path is secret/data//..." - - "Vault Agent Injector requires the vault-agent-injector pod to be running in the namespace's project" - - "ESO SecretStore uses Kubernetes auth — ServiceAccount must have a Vault policy granting read on the path" -``` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cd1a962..c4e4579 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,6 +7,12 @@ guardrails for the HelloNetworkWorld project. For the full human-readable version, see `CODING_STANDARDS.md`. +> **Agents & Skills**: Shared persona agents and reusable skills are in `.github/agents/` +> which is a git submodule of [`rl-agents-n-skills`](https://github.com/rloisell/rl-agents-n-skills). +> Project-specific subagents (network-policy, openshift-health, bc-gov-standards) are in `.claude/agents/`. +> VS Code discovers SKILL.md files in `.github/agents/` automatically. Claude Code loads the shared plugin via `.claude/settings.json`. +> To update shared agents: `cd .github/agents && git pull origin main && cd ../.. && git add .github/agents && git commit -m "chore: update rl-agents-n-skills submodule"` + --- ## Session Startup Protocol diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ace11ee --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".github/agents"] + path = .github/agents + url = https://github.com/rloisell/rl-agents-n-skills.git diff --git a/AI/nextSteps.md b/AI/nextSteps.md index a603ad2..080800a 100644 --- a/AI/nextSteps.md +++ b/AI/nextSteps.md @@ -25,7 +25,7 @@ | ✅ | **4** | Implement 001-project-scaffold (API health, DB migrations, React shell) | Large | API on 5200, Vite on 5175, MariaDB hnw_dev, CRUD verified | `main` | | ✅ | **5** | Add hnw-app Helm chart to tenant-gitops-be808f | Medium | PR #7 opened | `feat/hnw-gitops` | | ⬜ | **6** | GitHub Actions — build-and-push to Artifactory (hnw-api, hnw-frontend) | Medium | Depends on #5 | `feat/001-project-scaffold` | -| ⬜ | **7** | Implement 002-documentation-hub (standards reference pages) | Medium | Depends on #4 | `feat/002-documentation-hub` | +| ⬜ | **7** | Implement 002-network-docs-hub (network reference panel — OpenShift SDN, Silver/Gold/Emerald, DataClass/zones, NetworkPolicy patterns) | Medium | Direction change Mar 2026: narrowed from broad BC Gov standards hub to network-only reference. Depends on #4 | `feat/002-documentation-hub` | ### Tier 3 — Sprint 2 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6e4fcae --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +/* + * CLAUDE.md + * Ryan Loiselle — Developer / Architect + * GitHub Copilot — AI pair programmer / code generation + * June 2025 + * + * AI-assisted: project-level Claude Code instructions created as part of + * rl-agents-n-skills submodule migration; reviewed and directed by Ryan Loiselle. + */ + +# CLAUDE.md — HelloNetworkWorld + +This file provides project-level instructions for Claude Code. +Base instructions (shared personas, skills, subagents) are in `.github/agents/CLAUDE.md` +via the rl-agents-n-skills plugin. + +Project-specific subagents for this repo live in `.claude/agents/`. + +## Project purpose + +HelloNetworkWorld is a BC Government DevOps reference and infrastructure health-check +application deployed to OpenShift Emerald (`be808f` namespace): +- **API**: .NET 10 (ASP.NET Core), MariaDB (EF Core) +- **Frontend**: React/Vite with B.C. Government Design System +- **Deployment**: Emerald be808f (dev/test/prod/tools), ArgoCD GitOps via `bcgov-c/tenant-gitops-be808f` +- **Artifactory**: `artifacts.developer.gov.bc.ca/dbe8-docker-local/` + +Two primary functions: +1. **Standards Reference Hub** — curated BC Gov dev/design/security/deployment standards links +2. **Network Reachability Testing** — configurable cron-driven network test engine (NetworkTestDefinitions, Feature 007 NetworkPolicy automation) + +## Project-specific subagents + +These agents are in `.claude/agents/` and are specific to this project: + +| Agent | Purpose | +|-------|---------| +| `network-policy` | Generates/validates NetworkPolicy YAML for be808f; Feature 007 automation | +| `openshift-health` | OCP resource inspection, health check patterns, oc/argocd commands | +| `bc-gov-standards` | Enforces BC Gov design, security, deployment standards for be808f | + +## Architecture rules (project-specific) + +- Namespace: `be808f-{dev|test|prod|tools}` +- All pod labels must include `DataClass: Medium` +- Routes must have AVI annotation `aviinfrasetting.ako.vmware.com/name: dataclass-medium` +- GitOps repo: `bcgov-c/tenant-gitops-be808f` (Helm charts for hnw-app) +- Containers expose port 8080 only — never 80, 443, 5000 +- EF Core migrations on startup (`db.Database.Migrate()`) — never `EnsureCreated()` +- Auth Phase 1 = public; Phase 2 = Keycloak OIDC via `common-sso.justice.gov.bc.ca` + +## Submodule: rl-agents-n-skills + +Shared agents and skills live at `.github/agents/`, a git submodule pointing to +`https://github.com/rloisell/rl-agents-n-skills`. + +To update: +```bash +cd .github/agents && git pull origin main && cd ../.. +git add .github/agents +git commit -m "chore: update rl-agents-n-skills submodule" +``` + +Do NOT edit files inside `.github/agents/` directly — make changes in the +`rl-agents-n-skills` repo instead. Project-specific agents belong in `.claude/agents/`. diff --git a/kitty-specs/002-documentation-hub/plan.md b/kitty-specs/002-documentation-hub/plan.md index 2d0fc50..19f3eb8 100644 --- a/kitty-specs/002-documentation-hub/plan.md +++ b/kitty-specs/002-documentation-hub/plan.md @@ -1,17 +1,29 @@ -# Plan — Feature 002: Documentation Hub +# Plan — Feature 002: Network Documentation Hub + +> **Direction change (March 2026)**: Scope narrowed to network-specific reference content only. +> General BC Gov design/development/security docs are out of scope for this project. ## Phase 1 — API -- `ReferenceLink` entity already created in WP02 of feature 001 +- `ReferenceLink` entity already created in WP02 of feature 001 (no model changes required) - Implement `IReferenceLinkService` and `ReferenceLinkService` - Implement `ReferenceLinksController` (GET list, GET by id, POST, PUT, DELETE) -- Seed data via `IHostedService` on startup — all links from the catalogue in spec.md +- Seed data on startup — all links from the **network-focused** catalogue in spec.md + (4 categories: OpenShift Networking, Cluster Tiers, DataClass & Network Zone Model, + NetworkPolicy Patterns, SDN Guidance) +- Update `ReferenceLinkCategory` enum to remove non-network values (Design, Development, + Security, AIGuidance) and add network-specific values ## Phase 2 — Environment-aware links - Read `OPENSHIFT_NAMESPACE` and `OPENSHIFT_CLUSTER` from env vars -- Build ArgoCD, console, and Artifactory URLs dynamically +- Build ArgoCD app URL and OpenShift console namespace URL dynamically - Surface via a dedicated `GET /api/environment-info` endpoint -## Phase 3 — React docs page -- `/docs` route with category tabs (Design, Development, Security, OpenShift, AI Guidance) -- Link cards with icon, title, description, and external link arrow -- "Local Environment" section with dynamic links populated from `/api/environment-info` +## Phase 3 — React docs panel +- `/docs` route with category tabs: + - OpenShift Networking + - Cluster Tiers (Silver / Gold / Emerald) + - DataClass & Network Zones + - NetworkPolicy Patterns + - SDN Guidance +- Link cards with title, description, and external link indicator +- "Current Environment" section populated from `/api/environment-info` (ArgoCD, console) diff --git a/kitty-specs/002-documentation-hub/spec.md b/kitty-specs/002-documentation-hub/spec.md index 5b070da..9e56efe 100644 --- a/kitty-specs/002-documentation-hub/spec.md +++ b/kitty-specs/002-documentation-hub/spec.md @@ -1,33 +1,41 @@ -# Feature 002 — Documentation Hub +# Feature 002 — Network Documentation Hub **Author**: Ryan Loiselle — Developer / Architect **AI tool**: GitHub Copilot — AI pair programmer / code generation -**Updated**: February 2026 +**Updated**: March 2026 + +> **Direction change (March 2026)**: The original broad BC Gov standards documentation hub has +> been descoped. General DevOps/Design/Security docs are the responsibility of another team. +> This feature now covers **only network-related reference content** relevant to the HNW +> application's purpose: OpenShift SDN, cluster tier networking differences (Silver / Gold / +> Emerald), the BC Gov DataClass and network zone model, NetworkPolicy patterns, and egress +> guidance for operators configuring network test definitions. --- ## Overview -A curated, always-accessible documentation reference hub available at `/docs` in the deployed -application. Provides categorized links to all BC Gov development, design, security, and OpenShift -deployment standards. Also includes dynamic, environment-aware links that resolve to the actual -OpenShift console, ArgoCD, Artifactory, and other platform resources for the deployed environment. +A network-specific reference panel available at `/docs` in the deployed application. Provides +categorized links and inline guidance covering OpenShift networking, the BC Gov DataClass +labelling and network zone model, SDN behaviour differences across cluster tiers (Silver, Gold, +Emerald), common NetworkPolicy patterns, and practical egress guidance that helps operators +understand what egress policies their configured network tests will require. -This delivers on the project's primary mandate: "a singular location to access for all BC Gov -DevOps, Design, Security and Deployment standards for new projects." +This is a companion to the live network test engine — it answers the "why" and "how do I +configure access" questions that arise when a test shows a failure. --- ## User Stories -- As a developer working in a new BC Gov environment, I want to open `/docs` and find all - relevant BC Gov standards so I don't need to search for them across multiple sites. -- As a DevOps engineer, I want environment-specific links (ArgoCD, console, Artifactory) that - resolve to the actual URLs for the current deployment so I can click directly to the right place. +- As a DevOps engineer configuring a new network test, I want a reference panel that explains + what egress policy is needed so I know what to request or approve in the GitOps PR. +- As a platform operator reviewing an HNW-generated NetworkPolicy PR, I want inline guidance + on DataClass and network zone labelling so I can validate the PR is correctly scoped. +- As a developer new to BC Gov OpenShift, I want to understand the difference between Silver, + Gold, and Emerald networking so I know which policies apply to my namespace. - As a team lead, I want to add or edit reference links without a code deployment so I can - keep the hub current. -- As an AI assistant (GitHub Copilot), I want to reference the documentation hub spec so I know - which BC Gov resources to reference when giving guidance. + keep the panel current when platform docs change. --- @@ -35,13 +43,15 @@ DevOps, Design, Security and Deployment standards for new projects." ### Functional Requirements -- **FR-002-01**: `GET /api/reference-links` → paginated list of `ReferenceLink` entities, filterable by category +- **FR-002-01**: `GET /api/reference-links` → list of `ReferenceLink` entities, filterable by category - **FR-002-02**: `POST /api/reference-links` → create a new link (admin only in Phase 2; open in Phase 1) - **FR-002-03**: `PUT /api/reference-links/{id}` → update an existing link - **FR-002-04**: `DELETE /api/reference-links/{id}` → soft delete (sets `IsActive = false`) - **FR-002-05**: Seed data populates all links listed in the Reference Links Catalogue below on first startup - **FR-002-06**: Environment-aware links constructed from `OPENSHIFT_NAMESPACE` + `OPENSHIFT_CLUSTER` env vars -- **FR-002-07**: React `/docs` page renders links grouped by category with icons and descriptions + (ArgoCD app URL, OpenShift console namespace URL) +- **FR-002-07**: React `/docs` page renders links grouped by the network-focused categories below, + with descriptions and external link indicators ### Non-Functional Requirements @@ -53,66 +63,70 @@ DevOps, Design, Security and Deployment standards for new projects." ## Reference Links Catalogue -### Design +### OpenShift Networking | Title | URL | |-------|-----| -| BC Gov Design System | https://gov.bc.ca/designsystem | -| BC Gov Design System — GitHub | https://github.com/bcgov/design-system | -| BC Sans Font | https://www2.gov.bc.ca/gov/content/governments/services-for-government/policies-procedures/bc-visual-identity/bc-sans | -| BC Gov UX Guidelines | https://developer.gov.bc.ca/docs/default/component/design-system | +| OpenShift SDN Overview | https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html | +| Network Policy in OpenShift | https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html | +| Configuring egress NetworkPolicy | https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html | +| BC Gov Private Cloud — Network Policies | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/ | +| OVN-Kubernetes Overview | https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html | -### Development +### Cluster Tiers — Silver, Gold & Emerald | Title | URL | |-------|-----| -| BC Gov Developer Portal | https://developer.gov.bc.ca | -| Private Cloud PaaS Developer Docs | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs | -| BC Gov GitHub Organization | https://github.com/bcgov | -| BC Gov GitHub (Private) | https://github.com/bcgov-c | -| Rocket.Chat (Developer Chat) | https://chat.developer.gov.bc.ca | -| Common Get Token (GETOK) | https://getok.nrs.gov.bc.ca | -| Common SSO (Keycloak) | https://common-sso.justice.gov.bc.ca | -| BC Gov Digital Standards | https://digital.gov.bc.ca/policies-standards/dcss/digital-standards | - -### Security +| BC Gov Private Cloud Clusters | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters | +| Silver Cluster Networking Notes | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver | +| Gold Cluster Networking Notes | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold | +| Emerald Cluster Networking Notes | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald | +| AVI / NSX Advanced Load Balancer (Emerald) | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer | + +### DataClass & Network Zone Model | Title | URL | |-------|-----| -| BC Gov Information Security Policy | https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-policy | -| BC Gov STRA Process | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/security-and-privacy-compliance/stra-process | -| OWASP Top 10 | https://owasp.org/www-project-top-ten | -| CodeQL (GitHub Advanced Security) | https://codeql.github.com | -| Vault (Secrets) | https://vault.developer.gov.bc.ca | - -### OpenShift / Platform +| BC Gov DataClass Overview | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification | +| Network Zone Model (Public / Private / Restricted) | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones | +| DataClass Labels on Pods and Routes | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels | +| Impact of DataClass on Egress (Emerald) | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact | +| AVI InfraSettings — dataclass-medium vs dataclass-low | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings | +| BC Gov Information Security Classification Framework (ISCF) | https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification | +| BC Gov Network Security Zone Model — Zone A / B / C / DMZ (IMIT Standard 6.13) | https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security | + +### NetworkPolicy Patterns | Title | URL | |-------|-----| -| Emerald Console | https://console.apps.emerald.devops.gov.bc.ca | -| ArgoCD | https://gitops.apps.emerald.devops.gov.bc.ca | -| Artifactory | https://artifacts.developer.gov.bc.ca | -| Platform Registry | https://registry.developer.gov.bc.ca | -| Platform Status | https://status.developer.gov.bc.ca | -| Datree Policy Enforcer | https://datree.io | - -### AI Guidance +| Two-policy rule: ingress + egress | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule | +| DNS egress policy (UDP+TCP 53) | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns | +| Allow egress to external IP / CIDR | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr | +| Least-privilege NetworkPolicy approach | https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource | +| Common port reference (1521 Oracle, 1433 MSSQL, 5432 PG, 3306 MySQL) | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports | +| Third Party Gateway (3PG) / ExtraNet — external partner connectivity | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/ | + +### SDN Guidance | Title | URL | |-------|-----| -| GitHub Copilot for BC Gov | https://github.com/bcgov/developer-experience/blob/master/apps/artifactory/DEVHUB-README.md | -| AI Agent Skills (agentskills.io) | https://agentskills.io/home | -| Microsoft Agent Skills | https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/agent-skill | -| rl-project-template AI guidance | https://github.com/rloisell/rl-project-template | +| Default-deny ingress and egress on Emerald | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny | +| Calico vs OVN-Kubernetes | https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html | +| Troubleshooting NetworkPolicy | https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html | +| oc debug — testing network connectivity from a pod | https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html | +| SDN Security Classification — Low / Medium / High workload model (2022 OCIO SDN model) | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification | +| Medium/High workloads — internet egress via SSBC Forward Proxy only | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr | +| Zone adjacency rule — no zone hopping permitted | https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones | --- ## Success Criteria -- [ ] `GET /api/reference-links?category=Design` returns seeded design links -- [ ] React `/docs` page renders all categories with links; no 404s -- [ ] Environment links show correct ArgoCD/console URLs when `OPENSHIFT_NAMESPACE=be808f-dev` +- [ ] `GET /api/reference-links?category=OpenShift+Networking` returns seeded OpenShift networking links +- [ ] React `/docs` page renders all 4 network categories with links; no 404s on load +- [ ] Environment-aware ArgoCD and console links resolve correctly when `OPENSHIFT_NAMESPACE=be808f-dev` - [ ] Adding a custom link via `POST /api/reference-links` persists and appears in the list --- ## Out of Scope +- General BC Gov design, development, security, or AI guidance links (handled by a separate team) - Authentication for editing links (Phase 2 — feature 006) - Full-text search within links diff --git a/kitty-specs/002-documentation-hub/tasks/WP01 b/kitty-specs/002-documentation-hub/tasks/WP01 index 6adee78..ae1d538 100644 --- a/kitty-specs/002-documentation-hub/tasks/WP01 +++ b/kitty-specs/002-documentation-hub/tasks/WP01 @@ -1,15 +1,19 @@ -WP: Seed reference links database + controller +WP: Seed network reference links database + controller Feature: 002-documentation-hub Estimate: 1.5h Dependencies: WP01 (001-project-scaffold) Tasks: -1. Create EF Core initial migration covering all 4 entities -2. Add ReferenceLink seed data in ApplicationDbContext.OnModelCreating for all 7 categories -3. Implement and test GET /api/reference-links with optional ?category= filter -4. Verify DocsPage.jsx renders seeded links grouped by category tab +1. Update ReferenceLinkCategory enum — remove Design, Development, Security, AIGuidance; + add OpenShiftNetworking, ClusterTiers, DataClassAndZones, NetworkPolicyPatterns, SdnGuidance +2. Create EF Core migration for enum change +3. Add ReferenceLink seed data in ApplicationDbContext for all 5 network categories + (see spec.md Reference Links Catalogue for exact URLs) +4. Implement and test GET /api/reference-links with optional ?category= filter +5. Verify DocsPage.jsx renders seeded links grouped by the 5 network category tabs Acceptance: -- AC-002-01: All 7 category tabs render with ≥ 3 links each -- AC-002-02: Category filter query param returns correct subset -- AC-002-03: API returns 200 with at least 20 total links (seeded data) +- AC-002-01: All 5 network category tabs render with ≥ 3 links each +- AC-002-02: Category filter query param returns correct subset (e.g. ?category=NetworkPolicyPatterns) +- AC-002-03: API returns 200 with at least 20 total seeded links across all network categories +- AC-002-04: No Design / Development / Security / AIGuidance categories appear in the API or UI diff --git a/src/HNW.Data/ApplicationDbContext.cs b/src/HNW.Data/ApplicationDbContext.cs index ba4c2e7..7b89085 100644 --- a/src/HNW.Data/ApplicationDbContext.cs +++ b/src/HNW.Data/ApplicationDbContext.cs @@ -68,6 +68,47 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) e.Property(x => x.Description).HasMaxLength(500); e.Property(x => x.Category).HasConversion().HasMaxLength(50); }); + + // ── NETWORK REFERENCE LINKS SEED ───────────────────────────────────── + // Stable GUIDs ensure idempotency across restarts. Never change these IDs. + var seedDate = new DateTimeOffset(2026, 3, 30, 0, 0, 0, TimeSpan.Zero); + modelBuilder.Entity().HasData( + // ── OpenShift Networking ────────────────────────────────────────── + new ReferenceLink { Id = new Guid("11111111-1111-1111-1111-111111111101"), Category = ReferenceLinkCategory.OpenShiftNetworking, SortOrder = 10, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "OpenShift SDN Overview", Url = "https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html", Description = "How the OpenShift SDN plugin manages pod networking and inter-namespace isolation." }, + new ReferenceLink { Id = new Guid("11111111-1111-1111-1111-111111111102"), Category = ReferenceLinkCategory.OpenShiftNetworking, SortOrder = 20, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Network Policy in OpenShift", Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html", Description = "Overview of Kubernetes NetworkPolicy and how OpenShift enforces default-deny." }, + new ReferenceLink { Id = new Guid("11111111-1111-1111-1111-111111111103"), Category = ReferenceLinkCategory.OpenShiftNetworking, SortOrder = 30, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Configuring egress NetworkPolicy", Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html", Description = "Step-by-step guide to creating NetworkPolicy objects for egress traffic." }, + new ReferenceLink { Id = new Guid("11111111-1111-1111-1111-111111111104"), Category = ReferenceLinkCategory.OpenShiftNetworking, SortOrder = 40, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "BC Gov Private Cloud \u2014 Network Policies", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/", Description = "Platform-specific NetworkPolicy guidance for BC Gov OpenShift namespaces." }, + new ReferenceLink { Id = new Guid("11111111-1111-1111-1111-111111111105"), Category = ReferenceLinkCategory.OpenShiftNetworking, SortOrder = 50, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "OVN-Kubernetes Overview", Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html", Description = "OVN-Kubernetes network provider used in newer OpenShift clusters including Emerald." }, + // ── Cluster Tiers ───────────────────────────────────────────────── + new ReferenceLink { Id = new Guid("22222222-2222-2222-2222-222222222201"), Category = ReferenceLinkCategory.ClusterTiers, SortOrder = 10, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "BC Gov Private Cloud Clusters", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters", Description = "Comparison of Silver, Gold, and Emerald cluster capabilities, SDN implementations, and networking differences." }, + new ReferenceLink { Id = new Guid("22222222-2222-2222-2222-222222222202"), Category = ReferenceLinkCategory.ClusterTiers, SortOrder = 20, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Silver Cluster Networking Notes", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver", Description = "Silver-specific networking: OpenShift SDN, HAProxy router, ingress/egress defaults." }, + new ReferenceLink { Id = new Guid("22222222-2222-2222-2222-222222222203"), Category = ReferenceLinkCategory.ClusterTiers, SortOrder = 30, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Gold Cluster Networking Notes", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold", Description = "Gold-specific networking: OVN-Kubernetes, default-deny stance, multi-zone topology." }, + new ReferenceLink { Id = new Guid("22222222-2222-2222-2222-222222222204"), Category = ReferenceLinkCategory.ClusterTiers, SortOrder = 40, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Emerald Cluster Networking Notes", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald", Description = "Emerald-specific networking: strict default-deny ingress AND egress, AVI load balancer, DataClass enforcement." }, + new ReferenceLink { Id = new Guid("22222222-2222-2222-2222-222222222205"), Category = ReferenceLinkCategory.ClusterTiers, SortOrder = 50, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "AVI / NSX Advanced Load Balancer (Emerald)", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer", Description = "AVI replaces HAProxy on Emerald. Routes must carry the correct AVI InfraSettings annotation." }, + // ── DataClass & Network Zones ────────────────────────────────────── + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333301"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 10, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "BC Gov DataClass Overview", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification", Description = "How BC Government classifies data (Low / Medium / High) and what each class means for network access." }, + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333302"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 20, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Network Zone Model (Public / Private / Restricted)", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones", Description = "The three network zones and which data classes are permitted in each." }, + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333303"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 30, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "DataClass Labels on Pods and Routes", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels", Description = "Required pod label DataClass: Medium and how it interacts with Emerald's AVI InfraSettings." }, + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333304"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 40, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Impact of DataClass on Egress (Emerald)", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact", Description = "How DataClass labelling determines which egress network zones your pods can reach on Emerald." }, + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333305"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 50, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "AVI InfraSettings \u2014 dataclass-medium vs dataclass-low", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings", Description = "Only dataclass-medium has a registered VIP on Emerald. Never use dataclass-low." }, + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333306"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 60, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "BC Gov Information Security Classification Framework (ISCF)", Url = "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification", Description = "Foundation policy: defines Protected A / B / C information classes. The OpenShift DataClass label (Low / Medium / High) maps directly to ISCF levels \u2014 Low = public info, Medium = Protected A / lower Protected B, High = Protected B-C." }, + new ReferenceLink { Id = new Guid("33333333-3333-3333-3333-333333333307"), Category = ReferenceLinkCategory.DataClassAndZones, SortOrder = 70, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "BC Gov Network Security Zone Model \u2014 Zone A / B / C / DMZ", Url = "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security", Description = "Traditional OCIO zone model (IMIT Standard 6.13): Zone A = Restricted High Security (Protected B-C), Zone B = High Security (Protected A-low B), Zone C = Trusted Client (managed IDIR devices), DMZ = internet-facing proxies. SDN Low/Medium/High classifications map to these zones respectively." }, + // ── NetworkPolicy Patterns ───────────────────────────────────────── + new ReferenceLink { Id = new Guid("44444444-4444-4444-4444-444444444401"), Category = ReferenceLinkCategory.NetworkPolicyPatterns, SortOrder = 10, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Two-policy rule: ingress + egress", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule", Description = "Every network flow requires TWO policies: ingress on the receiver AND egress on the sender." }, + new ReferenceLink { Id = new Guid("44444444-4444-4444-4444-444444444402"), Category = ReferenceLinkCategory.NetworkPolicyPatterns, SortOrder = 20, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "DNS egress policy (UDP+TCP 53)", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns", Description = "All pods need an explicit DNS egress policy (UDP 53 + TCP 53) on Emerald \u2014 not included by default." }, + new ReferenceLink { Id = new Guid("44444444-4444-4444-4444-444444444403"), Category = ReferenceLinkCategory.NetworkPolicyPatterns, SortOrder = 30, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Allow egress to external IP / CIDR", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr", Description = "How to write a CIDR-based egress policy to allow traffic to external systems or on-prem networks." }, + new ReferenceLink { Id = new Guid("44444444-4444-4444-4444-444444444404"), Category = ReferenceLinkCategory.NetworkPolicyPatterns, SortOrder = 40, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Least-privilege NetworkPolicy", Url = "https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource", Description = "Kubernetes reference: only open the specific port and protocol needed \u2014 never wildcard egress." }, + new ReferenceLink { Id = new Guid("44444444-4444-4444-4444-444444444405"), Category = ReferenceLinkCategory.NetworkPolicyPatterns, SortOrder = 50, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Common port reference (Oracle 1521, MSSQL 1433, PG 5432, MySQL 3306)", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports", Description = "Standard database and service ports used when writing egress NetworkPolicy rules." }, + new ReferenceLink { Id = new Guid("44444444-4444-4444-4444-444444444406"), Category = ReferenceLinkCategory.NetworkPolicyPatterns, SortOrder = 60, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Third Party Gateway (3PG) / ExtraNet \u2014 external partner connectivity", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/", Description = "Connections to external government partners (other ministries, Crown corps, health authorities) must traverse the ExtraNet zone via a Third Party Gateway (3PG). Requires formal approval and a dedicated egress NetworkPolicy rule targeting the 3PG CIDR." }, + // ── SDN Guidance ────────────────────────────────────────────────── + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555501"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 10, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Default-deny ingress and egress on Emerald", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny", Description = "Emerald enforces default-deny on BOTH ingress and egress. Every flow must be explicitly allowed." }, + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555502"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 20, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Calico vs OVN-Kubernetes", Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html", Description = "Guidance on SDN migration and the differences between OpenShift SDN, Calico, and OVN-Kubernetes." }, + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555503"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 30, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Troubleshooting NetworkPolicy", Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html", Description = "How to view and inspect NetworkPolicy objects on a running cluster to diagnose connectivity failures." }, + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555504"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 40, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "oc debug \u2014 testing connectivity from a pod", Url = "https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html", Description = "Use oc debug to launch a temporary pod and test egress connectivity with curl, nc, or nslookup." }, + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555505"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 50, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "SDN Security Classification \u2014 Low / Medium / High workload model", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification", Description = "2022 BC Gov SDN model: Low = public info (DMZ-equivalent, internet accessible), Medium = Protected A (Zone B-equivalent, no direct internet), High = Protected B-C (Zone A-equivalent, internet blocked at guardrail). DataClass pod label must match workload classification." }, + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555506"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 60, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Medium/High workloads \u2014 internet egress via Forward Proxy only", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr", Description = "Medium security workloads cannot reach the internet directly \u2014 must use the SSBC SDN Forward Proxy (HTTP/HTTPS only). High security workloads require Ministry ISO (MISO) exemption for any internet access. Direct internet egress from Medium/High is denied at the guardrail." }, + new ReferenceLink { Id = new Guid("55555555-5555-5555-5555-555555555507"), Category = ReferenceLinkCategory.SdnGuidance, SortOrder = 70, IsActive = true, IsEnvironmentRelative = false, CreatedAt = seedDate, Title = "Zone adjacency rule \u2014 no zone hopping", Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones", Description = "BC Gov zone adjacency rule: communication is only permitted between adjacent zones. Traffic path: Internet \u2192 DMZ/Low \u2192 Medium \u2192 High. A session cannot be initiated directly from the internet into Medium or High zones. NetworkPolicy cannot bypass this adjacent-zones requirement." } + ); } } // end ApplicationDbContext diff --git a/src/HNW.Data/Migrations/20260330194250_SeedNetworkReferenceLinks.Designer.cs b/src/HNW.Data/Migrations/20260330194250_SeedNetworkReferenceLinks.Designer.cs new file mode 100644 index 0000000..8130a7b --- /dev/null +++ b/src/HNW.Data/Migrations/20260330194250_SeedNetworkReferenceLinks.Designer.cs @@ -0,0 +1,510 @@ +// +using System; +using HNW.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HNW.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260330194250_SeedNetworkReferenceLinks")] + partial class SeedNetworkReferenceLinks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CronExpression") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(253) + .HasColumnType("varchar(253)"); + + b.Property("ExpectedStatus") + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PolicyPrUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PolicyStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServiceType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("NetworkTestDefinitions"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("ExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsSuccess") + .HasColumnType("tinyint(1)"); + + b.Property("LatencyMs") + .HasColumnType("int"); + + b.Property("NetworkTestDefinitionId") + .HasColumnType("char(36)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ExecutedAt"); + + b.HasIndex("NetworkTestDefinitionId"); + + b.ToTable("NetworkTestResults"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestStateChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChangedAt") + .HasColumnType("datetime(6)"); + + b.Property("NetworkTestDefinitionId") + .HasColumnType("char(36)"); + + b.Property("NewState") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OldState") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("ChangedAt"); + + b.HasIndex("NetworkTestDefinitionId"); + + b.ToTable("NetworkTestStateChanges"); + }); + + modelBuilder.Entity("HNW.Data.Models.ReferenceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnvironmentRelative") + .HasColumnType("tinyint(1)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.HasKey("Id"); + + b.ToTable("ReferenceLinks"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-111111111101"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How the OpenShift SDN plugin manages pod networking and inter-namespace isolation.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "OpenShift SDN Overview", + Url = "https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111102"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Overview of Kubernetes NetworkPolicy and how OpenShift enforces default-deny.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Network Policy in OpenShift", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111103"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Step-by-step guide to creating NetworkPolicy objects for egress traffic.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Configuring egress NetworkPolicy", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111104"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Platform-specific NetworkPolicy guidance for BC Gov OpenShift namespaces.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "BC Gov Private Cloud — Network Policies", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111105"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "OVN-Kubernetes network provider used in newer OpenShift clusters including Emerald.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "OVN-Kubernetes Overview", + Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222201"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Comparison of Silver, Gold, and Emerald cluster capabilities, SDN implementations, and networking differences.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "BC Gov Private Cloud Clusters", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222202"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Silver-specific networking: OpenShift SDN, HAProxy router, ingress/egress defaults.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Silver Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222203"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Gold-specific networking: OVN-Kubernetes, default-deny stance, multi-zone topology.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Gold Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222204"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Emerald-specific networking: strict default-deny ingress AND egress, AVI load balancer, DataClass enforcement.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Emerald Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222205"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "AVI replaces HAProxy on Emerald. Routes must carry the correct AVI InfraSettings annotation.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "AVI / NSX Advanced Load Balancer (Emerald)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333301"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How BC Government classifies data (Low / Medium / High) and what each class means for network access.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "BC Gov DataClass Overview", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333302"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "The three network zones and which data classes are permitted in each.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Network Zone Model (Public / Private / Restricted)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333303"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Required pod label DataClass: Medium and how it interacts with Emerald's AVI InfraSettings.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "DataClass Labels on Pods and Routes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333304"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How DataClass labelling determines which egress network zones your pods can reach on Emerald.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Impact of DataClass on Egress (Emerald)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333305"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Only dataclass-medium has a registered VIP on Emerald. Never use dataclass-low.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "AVI InfraSettings — dataclass-medium vs dataclass-low", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444401"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Every network flow requires TWO policies: ingress on the receiver AND egress on the sender.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "Two-policy rule: ingress + egress", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444402"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "All pods need an explicit DNS egress policy (UDP 53 + TCP 53) on Emerald — not included by default.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "DNS egress policy (UDP+TCP 53)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444403"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How to write a CIDR-based egress policy to allow traffic to external systems or on-prem networks.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Allow egress to external IP / CIDR", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444404"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Kubernetes reference: only open the specific port and protocol needed — never wildcard egress.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Least-privilege NetworkPolicy", + Url = "https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444405"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Standard database and service ports used when writing egress NetworkPolicy rules.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "Common port reference (Oracle 1521, MSSQL 1433, PG 5432, MySQL 3306)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555501"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Emerald enforces default-deny on BOTH ingress and egress. Every flow must be explicitly allowed.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "Default-deny ingress and egress on Emerald", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555502"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Guidance on SDN migration and the differences between OpenShift SDN, Calico, and OVN-Kubernetes.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Calico vs OVN-Kubernetes", + Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555503"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How to view and inspect NetworkPolicy objects on a running cluster to diagnose connectivity failures.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Troubleshooting NetworkPolicy", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555504"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Use oc debug to launch a temporary pod and test egress connectivity with curl, nc, or nslookup.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "oc debug — testing connectivity from a pod", + Url = "https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html" + }); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestResult", b => + { + b.HasOne("HNW.Data.Models.NetworkTestDefinition", "NetworkTestDefinition") + .WithMany("Results") + .HasForeignKey("NetworkTestDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NetworkTestDefinition"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestStateChange", b => + { + b.HasOne("HNW.Data.Models.NetworkTestDefinition", "NetworkTestDefinition") + .WithMany("StateChanges") + .HasForeignKey("NetworkTestDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NetworkTestDefinition"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestDefinition", b => + { + b.Navigation("Results"); + + b.Navigation("StateChanges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/HNW.Data/Migrations/20260330194250_SeedNetworkReferenceLinks.cs b/src/HNW.Data/Migrations/20260330194250_SeedNetworkReferenceLinks.cs new file mode 100644 index 0000000..2495fc6 --- /dev/null +++ b/src/HNW.Data/Migrations/20260330194250_SeedNetworkReferenceLinks.cs @@ -0,0 +1,172 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace HNW.Data.Migrations +{ + /// + public partial class SeedNetworkReferenceLinks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "ReferenceLinks", + columns: new[] { "Id", "Category", "CreatedAt", "Description", "IsActive", "IsEnvironmentRelative", "SortOrder", "Title", "Url" }, + values: new object[,] + { + { new Guid("11111111-1111-1111-1111-111111111101"), "OpenShiftNetworking", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "How the OpenShift SDN plugin manages pod networking and inter-namespace isolation.", true, false, 10, "OpenShift SDN Overview", "https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html" }, + { new Guid("11111111-1111-1111-1111-111111111102"), "OpenShiftNetworking", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Overview of Kubernetes NetworkPolicy and how OpenShift enforces default-deny.", true, false, 20, "Network Policy in OpenShift", "https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html" }, + { new Guid("11111111-1111-1111-1111-111111111103"), "OpenShiftNetworking", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Step-by-step guide to creating NetworkPolicy objects for egress traffic.", true, false, 30, "Configuring egress NetworkPolicy", "https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html" }, + { new Guid("11111111-1111-1111-1111-111111111104"), "OpenShiftNetworking", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Platform-specific NetworkPolicy guidance for BC Gov OpenShift namespaces.", true, false, 40, "BC Gov Private Cloud — Network Policies", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" }, + { new Guid("11111111-1111-1111-1111-111111111105"), "OpenShiftNetworking", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "OVN-Kubernetes network provider used in newer OpenShift clusters including Emerald.", true, false, 50, "OVN-Kubernetes Overview", "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html" }, + { new Guid("22222222-2222-2222-2222-222222222201"), "ClusterTiers", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Comparison of Silver, Gold, and Emerald cluster capabilities, SDN implementations, and networking differences.", true, false, 10, "BC Gov Private Cloud Clusters", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters" }, + { new Guid("22222222-2222-2222-2222-222222222202"), "ClusterTiers", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Silver-specific networking: OpenShift SDN, HAProxy router, ingress/egress defaults.", true, false, 20, "Silver Cluster Networking Notes", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver" }, + { new Guid("22222222-2222-2222-2222-222222222203"), "ClusterTiers", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Gold-specific networking: OVN-Kubernetes, default-deny stance, multi-zone topology.", true, false, 30, "Gold Cluster Networking Notes", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold" }, + { new Guid("22222222-2222-2222-2222-222222222204"), "ClusterTiers", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Emerald-specific networking: strict default-deny ingress AND egress, AVI load balancer, DataClass enforcement.", true, false, 40, "Emerald Cluster Networking Notes", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald" }, + { new Guid("22222222-2222-2222-2222-222222222205"), "ClusterTiers", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "AVI replaces HAProxy on Emerald. Routes must carry the correct AVI InfraSettings annotation.", true, false, 50, "AVI / NSX Advanced Load Balancer (Emerald)", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer" }, + { new Guid("33333333-3333-3333-3333-333333333301"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "How BC Government classifies data (Low / Medium / High) and what each class means for network access.", true, false, 10, "BC Gov DataClass Overview", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" }, + { new Guid("33333333-3333-3333-3333-333333333302"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "The three network zones and which data classes are permitted in each.", true, false, 20, "Network Zone Model (Public / Private / Restricted)", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" }, + { new Guid("33333333-3333-3333-3333-333333333303"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Required pod label DataClass: Medium and how it interacts with Emerald's AVI InfraSettings.", true, false, 30, "DataClass Labels on Pods and Routes", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels" }, + { new Guid("33333333-3333-3333-3333-333333333304"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "How DataClass labelling determines which egress network zones your pods can reach on Emerald.", true, false, 40, "Impact of DataClass on Egress (Emerald)", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact" }, + { new Guid("33333333-3333-3333-3333-333333333305"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Only dataclass-medium has a registered VIP on Emerald. Never use dataclass-low.", true, false, 50, "AVI InfraSettings — dataclass-medium vs dataclass-low", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings" }, + { new Guid("44444444-4444-4444-4444-444444444401"), "NetworkPolicyPatterns", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Every network flow requires TWO policies: ingress on the receiver AND egress on the sender.", true, false, 10, "Two-policy rule: ingress + egress", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule" }, + { new Guid("44444444-4444-4444-4444-444444444402"), "NetworkPolicyPatterns", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "All pods need an explicit DNS egress policy (UDP 53 + TCP 53) on Emerald — not included by default.", true, false, 20, "DNS egress policy (UDP+TCP 53)", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns" }, + { new Guid("44444444-4444-4444-4444-444444444403"), "NetworkPolicyPatterns", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "How to write a CIDR-based egress policy to allow traffic to external systems or on-prem networks.", true, false, 30, "Allow egress to external IP / CIDR", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" }, + { new Guid("44444444-4444-4444-4444-444444444404"), "NetworkPolicyPatterns", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Kubernetes reference: only open the specific port and protocol needed — never wildcard egress.", true, false, 40, "Least-privilege NetworkPolicy", "https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource" }, + { new Guid("44444444-4444-4444-4444-444444444405"), "NetworkPolicyPatterns", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Standard database and service ports used when writing egress NetworkPolicy rules.", true, false, 50, "Common port reference (Oracle 1521, MSSQL 1433, PG 5432, MySQL 3306)", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports" }, + { new Guid("55555555-5555-5555-5555-555555555501"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Emerald enforces default-deny on BOTH ingress and egress. Every flow must be explicitly allowed.", true, false, 10, "Default-deny ingress and egress on Emerald", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny" }, + { new Guid("55555555-5555-5555-5555-555555555502"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Guidance on SDN migration and the differences between OpenShift SDN, Calico, and OVN-Kubernetes.", true, false, 20, "Calico vs OVN-Kubernetes", "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html" }, + { new Guid("55555555-5555-5555-5555-555555555503"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "How to view and inspect NetworkPolicy objects on a running cluster to diagnose connectivity failures.", true, false, 30, "Troubleshooting NetworkPolicy", "https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html" }, + { new Guid("55555555-5555-5555-5555-555555555504"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Use oc debug to launch a temporary pod and test egress connectivity with curl, nc, or nslookup.", true, false, 40, "oc debug — testing connectivity from a pod", "https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("11111111-1111-1111-1111-111111111101")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("11111111-1111-1111-1111-111111111102")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("11111111-1111-1111-1111-111111111103")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("11111111-1111-1111-1111-111111111104")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("11111111-1111-1111-1111-111111111105")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("22222222-2222-2222-2222-222222222201")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("22222222-2222-2222-2222-222222222202")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("22222222-2222-2222-2222-222222222203")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("22222222-2222-2222-2222-222222222204")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("22222222-2222-2222-2222-222222222205")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333301")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333302")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333303")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333304")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333305")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("44444444-4444-4444-4444-444444444401")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("44444444-4444-4444-4444-444444444402")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("44444444-4444-4444-4444-444444444403")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("44444444-4444-4444-4444-444444444404")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("44444444-4444-4444-4444-444444444405")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555501")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555502")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555503")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555504")); + } + } +} diff --git a/src/HNW.Data/Migrations/20260330200136_AddOcioNetworkStandardsLinks.Designer.cs b/src/HNW.Data/Migrations/20260330200136_AddOcioNetworkStandardsLinks.Designer.cs new file mode 100644 index 0000000..19d00c4 --- /dev/null +++ b/src/HNW.Data/Migrations/20260330200136_AddOcioNetworkStandardsLinks.Designer.cs @@ -0,0 +1,582 @@ +// +using System; +using HNW.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HNW.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260330200136_AddOcioNetworkStandardsLinks")] + partial class AddOcioNetworkStandardsLinks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CronExpression") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(253) + .HasColumnType("varchar(253)"); + + b.Property("ExpectedStatus") + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PolicyPrUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PolicyStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServiceType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("NetworkTestDefinitions"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("ExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsSuccess") + .HasColumnType("tinyint(1)"); + + b.Property("LatencyMs") + .HasColumnType("int"); + + b.Property("NetworkTestDefinitionId") + .HasColumnType("char(36)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ExecutedAt"); + + b.HasIndex("NetworkTestDefinitionId"); + + b.ToTable("NetworkTestResults"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestStateChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChangedAt") + .HasColumnType("datetime(6)"); + + b.Property("NetworkTestDefinitionId") + .HasColumnType("char(36)"); + + b.Property("NewState") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OldState") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("ChangedAt"); + + b.HasIndex("NetworkTestDefinitionId"); + + b.ToTable("NetworkTestStateChanges"); + }); + + modelBuilder.Entity("HNW.Data.Models.ReferenceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnvironmentRelative") + .HasColumnType("tinyint(1)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.HasKey("Id"); + + b.ToTable("ReferenceLinks"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-111111111101"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How the OpenShift SDN plugin manages pod networking and inter-namespace isolation.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "OpenShift SDN Overview", + Url = "https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111102"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Overview of Kubernetes NetworkPolicy and how OpenShift enforces default-deny.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Network Policy in OpenShift", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111103"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Step-by-step guide to creating NetworkPolicy objects for egress traffic.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Configuring egress NetworkPolicy", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111104"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Platform-specific NetworkPolicy guidance for BC Gov OpenShift namespaces.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "BC Gov Private Cloud — Network Policies", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111105"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "OVN-Kubernetes network provider used in newer OpenShift clusters including Emerald.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "OVN-Kubernetes Overview", + Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222201"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Comparison of Silver, Gold, and Emerald cluster capabilities, SDN implementations, and networking differences.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "BC Gov Private Cloud Clusters", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222202"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Silver-specific networking: OpenShift SDN, HAProxy router, ingress/egress defaults.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Silver Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222203"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Gold-specific networking: OVN-Kubernetes, default-deny stance, multi-zone topology.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Gold Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222204"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Emerald-specific networking: strict default-deny ingress AND egress, AVI load balancer, DataClass enforcement.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Emerald Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222205"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "AVI replaces HAProxy on Emerald. Routes must carry the correct AVI InfraSettings annotation.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "AVI / NSX Advanced Load Balancer (Emerald)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333301"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How BC Government classifies data (Low / Medium / High) and what each class means for network access.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "BC Gov DataClass Overview", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333302"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "The three network zones and which data classes are permitted in each.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Network Zone Model (Public / Private / Restricted)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333303"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Required pod label DataClass: Medium and how it interacts with Emerald's AVI InfraSettings.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "DataClass Labels on Pods and Routes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333304"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How DataClass labelling determines which egress network zones your pods can reach on Emerald.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Impact of DataClass on Egress (Emerald)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333305"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Only dataclass-medium has a registered VIP on Emerald. Never use dataclass-low.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "AVI InfraSettings — dataclass-medium vs dataclass-low", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333306"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Foundation policy: defines Protected A / B / C information classes. The OpenShift DataClass label (Low / Medium / High) maps directly to ISCF levels — Low = public info, Medium = Protected A / lower Protected B, High = Protected B-C.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 60, + Title = "BC Gov Information Security Classification Framework (ISCF)", + Url = "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333307"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Traditional OCIO zone model (IMIT Standard 6.13): Zone A = Restricted High Security (Protected B-C), Zone B = High Security (Protected A-low B), Zone C = Trusted Client (managed IDIR devices), DMZ = internet-facing proxies. SDN Low/Medium/High classifications map to these zones respectively.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 70, + Title = "BC Gov Network Security Zone Model — Zone A / B / C / DMZ", + Url = "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444401"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Every network flow requires TWO policies: ingress on the receiver AND egress on the sender.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "Two-policy rule: ingress + egress", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444402"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "All pods need an explicit DNS egress policy (UDP 53 + TCP 53) on Emerald — not included by default.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "DNS egress policy (UDP+TCP 53)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444403"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How to write a CIDR-based egress policy to allow traffic to external systems or on-prem networks.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Allow egress to external IP / CIDR", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444404"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Kubernetes reference: only open the specific port and protocol needed — never wildcard egress.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Least-privilege NetworkPolicy", + Url = "https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444405"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Standard database and service ports used when writing egress NetworkPolicy rules.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "Common port reference (Oracle 1521, MSSQL 1433, PG 5432, MySQL 3306)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444406"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Connections to external government partners (other ministries, Crown corps, health authorities) must traverse the ExtraNet zone via a Third Party Gateway (3PG). Requires formal approval and a dedicated egress NetworkPolicy rule targeting the 3PG CIDR.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 60, + Title = "Third Party Gateway (3PG) / ExtraNet — external partner connectivity", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555501"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Emerald enforces default-deny on BOTH ingress and egress. Every flow must be explicitly allowed.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "Default-deny ingress and egress on Emerald", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555502"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Guidance on SDN migration and the differences between OpenShift SDN, Calico, and OVN-Kubernetes.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Calico vs OVN-Kubernetes", + Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555503"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How to view and inspect NetworkPolicy objects on a running cluster to diagnose connectivity failures.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Troubleshooting NetworkPolicy", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555504"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Use oc debug to launch a temporary pod and test egress connectivity with curl, nc, or nslookup.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "oc debug — testing connectivity from a pod", + Url = "https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555505"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "2022 BC Gov SDN model: Low = public info (DMZ-equivalent, internet accessible), Medium = Protected A (Zone B-equivalent, no direct internet), High = Protected B-C (Zone A-equivalent, internet blocked at guardrail). DataClass pod label must match workload classification.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "SDN Security Classification — Low / Medium / High workload model", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555506"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Medium security workloads cannot reach the internet directly — must use the SSBC SDN Forward Proxy (HTTP/HTTPS only). High security workloads require Ministry ISO (MISO) exemption for any internet access. Direct internet egress from Medium/High is denied at the guardrail.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 60, + Title = "Medium/High workloads — internet egress via Forward Proxy only", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555507"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "BC Gov zone adjacency rule: communication is only permitted between adjacent zones. Traffic path: Internet → DMZ/Low → Medium → High. A session cannot be initiated directly from the internet into Medium or High zones. NetworkPolicy cannot bypass this adjacent-zones requirement.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 70, + Title = "Zone adjacency rule — no zone hopping", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" + }); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestResult", b => + { + b.HasOne("HNW.Data.Models.NetworkTestDefinition", "NetworkTestDefinition") + .WithMany("Results") + .HasForeignKey("NetworkTestDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NetworkTestDefinition"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestStateChange", b => + { + b.HasOne("HNW.Data.Models.NetworkTestDefinition", "NetworkTestDefinition") + .WithMany("StateChanges") + .HasForeignKey("NetworkTestDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NetworkTestDefinition"); + }); + + modelBuilder.Entity("HNW.Data.Models.NetworkTestDefinition", b => + { + b.Navigation("Results"); + + b.Navigation("StateChanges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/HNW.Data/Migrations/20260330200136_AddOcioNetworkStandardsLinks.cs b/src/HNW.Data/Migrations/20260330200136_AddOcioNetworkStandardsLinks.cs new file mode 100644 index 0000000..8fb7d9b --- /dev/null +++ b/src/HNW.Data/Migrations/20260330200136_AddOcioNetworkStandardsLinks.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace HNW.Data.Migrations +{ + /// + public partial class AddOcioNetworkStandardsLinks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "ReferenceLinks", + columns: new[] { "Id", "Category", "CreatedAt", "Description", "IsActive", "IsEnvironmentRelative", "SortOrder", "Title", "Url" }, + values: new object[,] + { + { new Guid("33333333-3333-3333-3333-333333333306"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Foundation policy: defines Protected A / B / C information classes. The OpenShift DataClass label (Low / Medium / High) maps directly to ISCF levels — Low = public info, Medium = Protected A / lower Protected B, High = Protected B-C.", true, false, 60, "BC Gov Information Security Classification Framework (ISCF)", "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification" }, + { new Guid("33333333-3333-3333-3333-333333333307"), "DataClassAndZones", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Traditional OCIO zone model (IMIT Standard 6.13): Zone A = Restricted High Security (Protected B-C), Zone B = High Security (Protected A-low B), Zone C = Trusted Client (managed IDIR devices), DMZ = internet-facing proxies. SDN Low/Medium/High classifications map to these zones respectively.", true, false, 70, "BC Gov Network Security Zone Model — Zone A / B / C / DMZ", "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security" }, + { new Guid("44444444-4444-4444-4444-444444444406"), "NetworkPolicyPatterns", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Connections to external government partners (other ministries, Crown corps, health authorities) must traverse the ExtraNet zone via a Third Party Gateway (3PG). Requires formal approval and a dedicated egress NetworkPolicy rule targeting the 3PG CIDR.", true, false, 60, "Third Party Gateway (3PG) / ExtraNet — external partner connectivity", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" }, + { new Guid("55555555-5555-5555-5555-555555555505"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "2022 BC Gov SDN model: Low = public info (DMZ-equivalent, internet accessible), Medium = Protected A (Zone B-equivalent, no direct internet), High = Protected B-C (Zone A-equivalent, internet blocked at guardrail). DataClass pod label must match workload classification.", true, false, 50, "SDN Security Classification — Low / Medium / High workload model", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" }, + { new Guid("55555555-5555-5555-5555-555555555506"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "Medium security workloads cannot reach the internet directly — must use the SSBC SDN Forward Proxy (HTTP/HTTPS only). High security workloads require Ministry ISO (MISO) exemption for any internet access. Direct internet egress from Medium/High is denied at the guardrail.", true, false, 60, "Medium/High workloads — internet egress via Forward Proxy only", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" }, + { new Guid("55555555-5555-5555-5555-555555555507"), "SdnGuidance", new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "BC Gov zone adjacency rule: communication is only permitted between adjacent zones. Traffic path: Internet → DMZ/Low → Medium → High. A session cannot be initiated directly from the internet into Medium or High zones. NetworkPolicy cannot bypass this adjacent-zones requirement.", true, false, 70, "Zone adjacency rule — no zone hopping", "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333306")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("33333333-3333-3333-3333-333333333307")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("44444444-4444-4444-4444-444444444406")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555505")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555506")); + + migrationBuilder.DeleteData( + table: "ReferenceLinks", + keyColumn: "Id", + keyValue: new Guid("55555555-5555-5555-5555-555555555507")); + } + } +} diff --git a/src/HNW.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/HNW.Data/Migrations/ApplicationDbContextModelSnapshot.cs index edc2a58..5668d3c 100644 --- a/src/HNW.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/HNW.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -181,6 +181,368 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.ToTable("ReferenceLinks"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-111111111101"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How the OpenShift SDN plugin manages pod networking and inter-namespace isolation.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "OpenShift SDN Overview", + Url = "https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111102"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Overview of Kubernetes NetworkPolicy and how OpenShift enforces default-deny.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Network Policy in OpenShift", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111103"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Step-by-step guide to creating NetworkPolicy objects for egress traffic.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Configuring egress NetworkPolicy", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111104"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Platform-specific NetworkPolicy guidance for BC Gov OpenShift namespaces.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "BC Gov Private Cloud — Network Policies", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" + }, + new + { + Id = new Guid("11111111-1111-1111-1111-111111111105"), + Category = "OpenShiftNetworking", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "OVN-Kubernetes network provider used in newer OpenShift clusters including Emerald.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "OVN-Kubernetes Overview", + Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222201"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Comparison of Silver, Gold, and Emerald cluster capabilities, SDN implementations, and networking differences.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "BC Gov Private Cloud Clusters", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222202"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Silver-specific networking: OpenShift SDN, HAProxy router, ingress/egress defaults.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Silver Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222203"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Gold-specific networking: OVN-Kubernetes, default-deny stance, multi-zone topology.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Gold Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222204"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Emerald-specific networking: strict default-deny ingress AND egress, AVI load balancer, DataClass enforcement.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Emerald Cluster Networking Notes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald" + }, + new + { + Id = new Guid("22222222-2222-2222-2222-222222222205"), + Category = "ClusterTiers", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "AVI replaces HAProxy on Emerald. Routes must carry the correct AVI InfraSettings annotation.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "AVI / NSX Advanced Load Balancer (Emerald)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333301"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How BC Government classifies data (Low / Medium / High) and what each class means for network access.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "BC Gov DataClass Overview", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333302"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "The three network zones and which data classes are permitted in each.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Network Zone Model (Public / Private / Restricted)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333303"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Required pod label DataClass: Medium and how it interacts with Emerald's AVI InfraSettings.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "DataClass Labels on Pods and Routes", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333304"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How DataClass labelling determines which egress network zones your pods can reach on Emerald.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Impact of DataClass on Egress (Emerald)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333305"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Only dataclass-medium has a registered VIP on Emerald. Never use dataclass-low.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "AVI InfraSettings — dataclass-medium vs dataclass-low", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333306"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Foundation policy: defines Protected A / B / C information classes. The OpenShift DataClass label (Low / Medium / High) maps directly to ISCF levels — Low = public info, Medium = Protected A / lower Protected B, High = Protected B-C.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 60, + Title = "BC Gov Information Security Classification Framework (ISCF)", + Url = "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification" + }, + new + { + Id = new Guid("33333333-3333-3333-3333-333333333307"), + Category = "DataClassAndZones", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Traditional OCIO zone model (IMIT Standard 6.13): Zone A = Restricted High Security (Protected B-C), Zone B = High Security (Protected A-low B), Zone C = Trusted Client (managed IDIR devices), DMZ = internet-facing proxies. SDN Low/Medium/High classifications map to these zones respectively.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 70, + Title = "BC Gov Network Security Zone Model — Zone A / B / C / DMZ", + Url = "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444401"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Every network flow requires TWO policies: ingress on the receiver AND egress on the sender.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "Two-policy rule: ingress + egress", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444402"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "All pods need an explicit DNS egress policy (UDP 53 + TCP 53) on Emerald — not included by default.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "DNS egress policy (UDP+TCP 53)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444403"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How to write a CIDR-based egress policy to allow traffic to external systems or on-prem networks.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Allow egress to external IP / CIDR", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444404"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Kubernetes reference: only open the specific port and protocol needed — never wildcard egress.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "Least-privilege NetworkPolicy", + Url = "https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444405"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Standard database and service ports used when writing egress NetworkPolicy rules.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "Common port reference (Oracle 1521, MSSQL 1433, PG 5432, MySQL 3306)", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports" + }, + new + { + Id = new Guid("44444444-4444-4444-4444-444444444406"), + Category = "NetworkPolicyPatterns", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Connections to external government partners (other ministries, Crown corps, health authorities) must traverse the ExtraNet zone via a Third Party Gateway (3PG). Requires formal approval and a dedicated egress NetworkPolicy rule targeting the 3PG CIDR.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 60, + Title = "Third Party Gateway (3PG) / ExtraNet — external partner connectivity", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555501"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Emerald enforces default-deny on BOTH ingress and egress. Every flow must be explicitly allowed.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 10, + Title = "Default-deny ingress and egress on Emerald", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555502"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Guidance on SDN migration and the differences between OpenShift SDN, Calico, and OVN-Kubernetes.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 20, + Title = "Calico vs OVN-Kubernetes", + Url = "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555503"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "How to view and inspect NetworkPolicy objects on a running cluster to diagnose connectivity failures.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 30, + Title = "Troubleshooting NetworkPolicy", + Url = "https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555504"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Use oc debug to launch a temporary pod and test egress connectivity with curl, nc, or nslookup.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 40, + Title = "oc debug — testing connectivity from a pod", + Url = "https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555505"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "2022 BC Gov SDN model: Low = public info (DMZ-equivalent, internet accessible), Medium = Protected A (Zone B-equivalent, no direct internet), High = Protected B-C (Zone A-equivalent, internet blocked at guardrail). DataClass pod label must match workload classification.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 50, + Title = "SDN Security Classification — Low / Medium / High workload model", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555506"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Medium security workloads cannot reach the internet directly — must use the SSBC SDN Forward Proxy (HTTP/HTTPS only). High security workloads require Ministry ISO (MISO) exemption for any internet access. Direct internet egress from Medium/High is denied at the guardrail.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 60, + Title = "Medium/High workloads — internet egress via Forward Proxy only", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr" + }, + new + { + Id = new Guid("55555555-5555-5555-5555-555555555507"), + Category = "SdnGuidance", + CreatedAt = new DateTimeOffset(new DateTime(2026, 3, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "BC Gov zone adjacency rule: communication is only permitted between adjacent zones. Traffic path: Internet → DMZ/Low → Medium → High. A session cannot be initiated directly from the internet into Medium or High zones. NetworkPolicy cannot bypass this adjacent-zones requirement.", + IsActive = true, + IsEnvironmentRelative = false, + SortOrder = 70, + Title = "Zone adjacency rule — no zone hopping", + Url = "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones" + }); }); modelBuilder.Entity("HNW.Data.Models.NetworkTestResult", b => diff --git a/src/HNW.Data/Models/ReferenceLink.cs b/src/HNW.Data/Models/ReferenceLink.cs index 8cce145..be93aba 100644 --- a/src/HNW.Data/Models/ReferenceLink.cs +++ b/src/HNW.Data/Models/ReferenceLink.cs @@ -12,7 +12,8 @@ namespace HNW.Data.Models; /// -/// A documentation reference link displayed on the /docs page. +/// A network reference link displayed on the /docs panel. +/// Covers OpenShift SDN, cluster tier networking, DataClass/zones, NetworkPolicy patterns. /// IsEnvironmentRelative links are built dynamically from cluster context env vars. /// public class ReferenceLink @@ -40,15 +41,14 @@ public class ReferenceLink // ── ENUMS ───────────────────────────────────────────────────────────────────── /// -/// Display category for reference links in the documentation hub. +/// Display category for network reference links in the /docs panel. +/// Pivoted March 2026: narrowed from general BC Gov standards to network-specific content. /// public enum ReferenceLinkCategory { - Design, // BC Gov Design System, BC Sans, UX guidelines - Development, // Developer portal, GitHub orgs, Rocket.Chat - Security, // Info security policy, STRA, CodeQL, Vault - OpenShift, // Emerald console, ArgoCD, Artifactory, Platform Registry - GitOps, // ArgoCD, Helm, GitHub Actions patterns - AIGuidance, // Copilot, agent skills, rl-project-template AI docs - LocalEnvironment // Dynamic links specific to this deployment (environment-relative) + OpenShiftNetworking, // OpenShift SDN, OVN-Kubernetes, NetworkPolicy overview + ClusterTiers, // Silver / Gold / Emerald cluster networking differences + DataClassAndZones, // BC Gov DataClass labels, network zone model, Emerald AVI impact + NetworkPolicyPatterns, // Two-policy rule, DNS egress, CIDR allowances, common ports + SdnGuidance // Default-deny, troubleshooting, oc debug, Calico vs OVN } diff --git a/src/HNW.WebClient/src/App.jsx b/src/HNW.WebClient/src/App.jsx index dc10ac4..98e1670 100644 --- a/src/HNW.WebClient/src/App.jsx +++ b/src/HNW.WebClient/src/App.jsx @@ -32,7 +32,7 @@ export default function App() { Network Tests isActive ? 'hnw-nav__link hnw-nav__link--active' : 'hnw-nav__link'}> - Standards & Docs + Network Docs @@ -66,7 +66,7 @@ function BCGovHeader() {
HelloNetworkWorld - Network Health & BC Gov Standards + Network Health & Connectivity Testing
diff --git a/src/HNW.WebClient/src/pages/DocsPage.jsx b/src/HNW.WebClient/src/pages/DocsPage.jsx index 1ceceec..b02d90f 100644 --- a/src/HNW.WebClient/src/pages/DocsPage.jsx +++ b/src/HNW.WebClient/src/pages/DocsPage.jsx @@ -1,16 +1,16 @@ /** - * DocsPage.jsx — BC Gov Standards Documentation Hub + * DocsPage.jsx — Network Reference Panel * Ryan Loiselle — Developer / Architect * GitHub Copilot — AI pair programmer / code generation - * February 2026 + * March 2026 * - * Provides a tabbed reference hub of all BC Government DevOps, Design, - * Security, and Deployment standards, with dynamic cluster-relative links. + * Network-focused reference panel: OpenShift SDN, Silver/Gold/Emerald cluster + * networking, BC Gov DataClass and zone model, NetworkPolicy patterns, SDN guidance. * Links are fetched from the API (ReferenceLinks table), falling back to * hardcoded defaults if the API is unavailable. * - * Implements: 002-documentation-hub - * AI-assisted: component scaffold + tab navigation; + * Implements: 002-documentation-hub (pivoted March 2026 — network reference only) + * AI-assisted: category + static fallback update for network pivot; * reviewed and directed by Ryan Loiselle. */ @@ -22,160 +22,182 @@ import { getReferenceLinks } from "../api/networkTestsApi"; /** @type {Record>} */ const STATIC_LINKS = { - Design: [ + OpenShiftNetworking: [ { - title: "BC Gov Design System", - url: "https://design.gov.bc.ca", - description: "Official BC Government design tokens, components, and style guides.", + title: "OpenShift SDN Overview", + url: "https://docs.openshift.com/container-platform/4.14/networking/openshift_sdn/about-openshift-sdn.html", + description: "How the OpenShift SDN plugin manages pod networking and inter-namespace isolation.", }, { - title: "Design Tokens npm (@bcgov/design-tokens)", - url: "https://www.npmjs.com/package/@bcgov/design-tokens", - description: "CSS custom properties and design tokens for React/web projects.", + title: "Network Policy in OpenShift", + url: "https://docs.openshift.com/container-platform/4.14/networking/network_policy/about-network-policy.html", + description: "Overview of Kubernetes NetworkPolicy and how OpenShift enforces default-deny.", }, { - title: "BC Gov Font (BCSans)", - url: "https://fonts2.gov.bc.ca", - description: "BCSans web font for official BC Gov applications.", + title: "Configuring egress NetworkPolicy", + url: "https://docs.openshift.com/container-platform/4.14/networking/network_policy/creating-network-policy.html", + description: "Step-by-step guide to creating NetworkPolicy objects for egress traffic.", + }, + { + title: "BC Gov Private Cloud — Network Policies", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/", + description: "Platform-specific NetworkPolicy guidance for BC Gov OpenShift namespaces.", + }, + { + title: "OVN-Kubernetes Overview", + url: "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/about-ovn-kubernetes.html", + description: "OVN-Kubernetes network provider used in newer OpenShift clusters including Emerald.", }, ], - Development: [ + ClusterTiers: [ { - title: "DevOps Platform Services Docs", - url: "https://docs.developer.gov.bc.ca", - description: "Comprehensive guide to building and deploying on OpenShift Emerald.", + title: "BC Gov Private Cloud Clusters", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters", + description: "Comparison of Silver, Gold, and Emerald cluster capabilities, SDN implementations, and networking differences.", }, { - title: "BC Gov GitHub Org (bcgov-c)", - url: "https://github.com/bcgov-c", - description: "Internal (private) GitHub organization for BC Gov projects.", + title: "Silver Cluster Networking Notes", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#silver", + description: "Silver-specific networking: OpenShift SDN, HAProxy router, ingress/egress defaults.", }, { - title: "Artifactory (dbe8-docker-local)", - url: "https://artifacts.developer.gov.bc.ca", - description: "Container image registry for be808f namespace. Push images here.", + title: "Gold Cluster Networking Notes", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#gold", + description: "Gold-specific networking: OVN-Kubernetes, default-deny stance, multi-zone topology.", }, { - title: "OpenShift Emerald Console", - url: "https://console.apps.emerald.devops.gov.bc.ca", - description: "OpenShift cluster console for Emerald (requires VPN/IDIR).", + title: "Emerald Cluster Networking Notes", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#emerald", + description: "Emerald-specific networking: strict default-deny ingress AND egress, AVI load balancer, DataClass enforcement.", }, - ], - Security: [ { - title: "IMIT Cyber Security Policy", - url: "https://www2.gov.bc.ca/gov/content/governments/services-for-government/policies-procedures/cyber-security/cyber-security-policy", - description: "BC Government cyber security policy — DataClass and handling requirements.", + title: "AVI / NSX Advanced Load Balancer (Emerald)", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#avi-nsx-advanced-load-balancer", + description: "AVI replaces HAProxy on Emerald. Routes must carry the correct AVI InfraSettings annotation.", }, + ], + DataClassAndZones: [ { - title: "Information Security Classification", - url: "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification", - description: "How to classify data (Protected A/B/C, etc.) for BC Gov systems.", + title: "BC Gov DataClass Overview", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification", + description: "How BC Government classifies data (Low / Medium / High) and what each class means for network access.", }, { - title: "Common SSO (Keycloak)", - url: "https://common-sso.justice.gov.bc.ca", - description: "Justice common hosted Keycloak OIDC. Used for Phase 2 auth in HNW.", + title: "Network Zone Model (Public / Private / Restricted)", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones", + description: "The three network zones and which data classes are permitted in each.", }, { - title: "GETOK — Service Client Registration", - url: "https://getok.nrs.gov.bc.ca", - description: "Register a Keycloak service client for machine-to-machine auth.", + title: "DataClass Labels on Pods and Routes", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#labels", + description: "Required pod label DataClass: Medium and how it interacts with Emerald's AVI InfraSettings.", }, - ], - OpenShift: [ { - title: "Deploy to OpenShift", - url: "https://docs.developer.gov.bc.ca/deploy-to-openshift/", - description: "DevOps Platform guide to deploying workloads on OpenShift.", + title: "Impact of DataClass on Egress (Emerald)", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#emerald-impact", + description: "How DataClass labelling determines which egress network zones your pods can reach on Emerald.", }, { - title: "OpenShift NetworkPolicy", - url: "https://docs.developer.gov.bc.ca/openshift-network-policies/", - description: "Configuring NetworkPolicies for namespaces on OpenShift.", + title: "AVI InfraSettings — dataclass-medium vs dataclass-low", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/avi-infrasettings", + description: "Only dataclass-medium has a registered VIP on Emerald. Never use dataclass-low.", }, { - title: "Resource Tuning", - url: "https://docs.developer.gov.bc.ca/openshift-resource-tuning/", - description: "Tuning CPU/memory requests and limits for OpenShift pods.", + title: "BC Gov Information Security Classification Framework (ISCF)", + url: "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security/information-security-classification", + description: "Foundation policy defining Protected A / B / C information classes. The OpenShift DataClass label (Low / Medium / High) maps directly to ISCF levels — Low = public info, Medium = Protected A / lower Protected B, High = Protected B-C.", }, { - title: "Emerald AVI / HAProxy", - url: "https://docs.developer.gov.bc.ca/openshift-routes/", - description: "Route and AVI InfraSettings on Emerald. Always use dataclass-medium.", + title: "BC Gov Network Security Zone Model — Zone A / B / C / DMZ", + url: "https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/information-security", + description: "Traditional OCIO zone model (IMIT Standard 6.13): Zone A = Restricted High Security (Protected B-C), Zone B = High Security (Protected A), Zone C = Trusted Client (managed IDIR devices), DMZ = internet-facing. SDN Low/Medium/High classifications map to these historic zones respectively.", }, ], - GitOps: [ + NetworkPolicyPatterns: [ { - title: "ArgoCD (Platform)", - url: "https://argocd.developer.gov.bc.ca", - description: "GitOps continuous delivery — syncs Helm charts from tenant-gitops-be808f.", + title: "Two-policy rule: ingress + egress", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#two-policy-rule", + description: "Every network flow requires TWO policies: ingress on the receiver AND egress on the sender.", }, { - title: "tenant-gitops-be808f (GitHub)", - url: "https://github.com/bcgov-c/tenant-gitops-be808f", - description: "Shared GitOps repo for the be808f namespace. HNW Helm chart lives here.", + title: "DNS egress policy (UDP+TCP 53)", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#dns", + description: "All pods need an explicit DNS egress policy (UDP 53 + TCP 53) on Emerald — not included by default.", }, { - title: "Helm Docs", - url: "https://helm.sh/docs/", - description: "Official Helm 3 documentation for chart authoring.", + title: "Allow egress to external IP / CIDR", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr", + description: "How to write a CIDR-based egress policy to allow traffic to external systems or on-prem networks.", }, - ], - AIGuidance: [ { - title: "GitHub Copilot Docs", - url: "https://docs.github.com/en/copilot", - description: "GitHub Copilot code assistant documentation.", + title: "Least-privilege NetworkPolicy", + url: "https://kubernetes.io/docs/concepts/services-networking/network-policies/#the-networkpolicy-resource", + description: "Kubernetes reference: only open the specific port and protocol needed — never wildcard egress.", }, { - title: "HNW Copilot Instructions", - url: "https://github.com/bcgov-c/HelloNetworkWorld/blob/main/.github/copilot-instructions.md", - description: "Project-specific AI guardrails and domain rules for HNW development.", + title: "Common port reference (Oracle 1521, MSSQL 1433, PG 5432, MySQL 3306)", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#common-ports", + description: "Standard database and service ports used when writing egress NetworkPolicy rules.", }, { - title: "Spec-Kitty (Spec-driven development)", - url: "https://github.com/bcgov-c/HelloNetworkWorld/tree/main/kitty-specs", - description: "Feature spec files, plans, and WP task tracking for HNW.", + title: "Third Party Gateway (3PG) / ExtraNet — external partner connectivity", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/", + description: "Connections to external government partners (other ministries, Crown corps, health authorities) must traverse the ExtraNet zone via a Third Party Gateway (3PG). Requires formal approval and a dedicated egress NetworkPolicy rule targeting the 3PG CIDR.", }, ], - LocalEnvironment: [ + SdnGuidance: [ + { + title: "Default-deny ingress and egress on Emerald", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/openshift-clusters#default-deny", + description: "Emerald enforces default-deny on BOTH ingress and egress. Every flow must be explicitly allowed.", + }, + { + title: "Calico vs OVN-Kubernetes", + url: "https://docs.openshift.com/container-platform/4.14/networking/ovn_kubernetes_network_provider/migrate-from-openshift-sdn.html", + description: "Guidance on SDN migration and the differences between OpenShift SDN, Calico, and OVN-Kubernetes.", + }, + { + title: "Troubleshooting NetworkPolicy", + url: "https://docs.openshift.com/container-platform/4.14/networking/network_policy/viewing-network-policy.html", + description: "How to view and inspect NetworkPolicy objects on a running cluster to diagnose connectivity failures.", + }, + { + title: "oc debug — testing connectivity from a pod", + url: "https://docs.openshift.com/container-platform/4.14/support/troubleshooting/troubleshooting-network-issues.html", + description: "Use oc debug to launch a temporary pod and test egress connectivity with curl, nc, or nslookup.", + }, { - title: "Local Development Guide", - url: "https://github.com/bcgov-c/HelloNetworkWorld/blob/main/docs/local-development/README.md", - description: "How to run the API (port 5200), frontend (port 5175), and MariaDB locally.", + title: "SDN Security Classification — Low / Medium / High workload model", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification", + description: "2022 BC Gov SDN model: Low = public info (DMZ-equivalent, internet accessible), Medium = Protected A (no direct internet), High = Protected B-C (internet blocked at guardrail). DataClass pod label must match workload classification.", }, { - title: "Podman Desktop", - url: "https://podman-desktop.io", - description: "Container management tool used instead of Docker on BC Gov developer machines.", + title: "Medium/High workloads — internet egress via Forward Proxy only", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/openshift-projects-and-access/network-policies/#egress-cidr", + description: "Medium security workloads cannot reach the internet directly — must use the SSBC SDN Forward Proxy (HTTP/HTTPS only). High workloads require Ministry ISO (MISO) exemption. Direct internet egress from Medium/High is denied at the SDN guardrail.", }, { - title: "EF Core Migrations", - url: "https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/", - description: "Entity Framework Core migration docs — used for HNW database schema management.", + title: "Zone adjacency rule — no zone hopping", + url: "https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/platform-architecture-reference/network-zones-and-data-classification#network-zones", + description: "BC Gov zone adjacency rule: communication is only permitted between adjacent zones. Traffic path: Internet → DMZ/Low → Medium → High. A session cannot be initiated directly from the internet into Medium or High zones.", }, ], }; const CATEGORY_ORDER = [ - "Design", - "Development", - "Security", - "OpenShift", - "GitOps", - "AIGuidance", - "LocalEnvironment", + "OpenShiftNetworking", + "ClusterTiers", + "DataClassAndZones", + "NetworkPolicyPatterns", + "SdnGuidance", ]; const CATEGORY_LABELS = { - Design: "Design", - Development: "Development", - Security: "Security", - OpenShift: "OpenShift", - GitOps: "GitOps / ArgoCD", - AIGuidance: "AI Guidance", - LocalEnvironment: "Local Dev", + OpenShiftNetworking: "OpenShift Networking", + ClusterTiers: "Silver / Gold / Emerald", + DataClassAndZones: "DataClass & Zones", + NetworkPolicyPatterns: "NetworkPolicy Patterns", + SdnGuidance: "SDN Guidance", }; // ── Main page ─────────────────────────────────────────────────────────────── @@ -204,10 +226,10 @@ export default function DocsPage() { return ( <>
-

BC Gov Standards Hub

+

Network Reference

- Authoritative reference links for design, development, security, and deployment standards - used in BC Government projects. + OpenShift SDN, cluster tier networking, BC Gov DataClass and zone model, + NetworkPolicy patterns, and egress guidance for configured network tests.

@@ -259,17 +281,17 @@ export default function DocsPage() { )} - {/* Cluster-relative links note */} + {/* Environment context note */}
-

Cluster-relative links

+

Environment context

- Some links (OpenShift console, ArgoCD, route URLs) adapt based on the current deployment + Some links (OpenShift console, ArgoCD app) adapt based on the current deployment environment. When deployed to OpenShift, the API returns environment-specific URLs (e.g., https://hnw-be808f-dev.apps.emerald.devops.gov.bc.ca).

- To add or customise links, use the ReferenceLinks database table or the API - endpoint POST /api/reference-links. + To add additional network reference links, use POST /api/reference-links + or edit the ReferenceLinks table directly.