diff --git a/.claude/hooks/statusline.sh b/.claude/hooks/statusline.sh new file mode 100755 index 0000000000..6690eba183 --- /dev/null +++ b/.claude/hooks/statusline.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Project statusline: extends user statusline with osac + ai-workflows sync status +# Opt out: set CLAUDE_REPO_STATUSLINE_DISABLED=1 in .claude/settings.local.json env + +input=$(cat) + +if [[ "${CLAUDE_REPO_STATUSLINE_DISABLED:-}" == "1" ]]; then + exit 0 +fi + +project_dir="${1:-}" + +if command -v jq >/dev/null 2>&1 && [[ -f "${HOME}/.claude/settings.json" ]]; then + USER_STATUSLINE_CMD=$(jq -r '.statusLine.command // empty' "${HOME}/.claude/settings.json" 2>/dev/null) + if [[ -n "${USER_STATUSLINE_CMD}" ]]; then + printf "%s\n" "$input" | bash -c "${USER_STATUSLINE_CMD}" + fi +fi + +GREEN='\033[32m' +YELLOW='\033[33m' +GRAY='\033[90m' +RESET='\033[0m' + +log_info() { printf '%b%s%b' "$GREEN" "$1" "$RESET"; } +log_warning() { printf '%b%s%b' "$YELLOW" "$1" "$RESET"; } +log_muted() { printf '%b%s%b' "$GRAY" "$1" "$RESET"; } + +repo_status() { + local dir="$1" name="$2" + [[ -d "$dir" ]] || { log_muted "$name: not found"; return; } + + local branch behind + branch=$(git -C "$dir" branch --show-current 2>/dev/null) || branch="detached" + [[ -n "$branch" ]] || branch="detached" + behind=$(git -C "$dir" rev-list HEAD..origin/main --count 2>/dev/null) || { log_muted "$name: $branch ?"; return; } + + if [[ "$behind" -eq 0 ]]; then + log_info "$name: $branch ✓" + else + log_warning "$name: $branch ↓${behind} behind" + fi +} + +REPO_DIR="${project_dir:-$(printf '%s' "$input" | jq -r '.workspace.project_dir // empty' 2>/dev/null)}" +AI_DIR="${HOME}/.ai-workflows" + +ws=$(repo_status "$REPO_DIR" "osac") +ai=$(repo_status "$AI_DIR" "ai-workflows") + +printf '%b %b %b\n' "$ws" "${GRAY}|${RESET}" "$ai" diff --git a/.claude/hooks/update-ai-context.sh b/.claude/hooks/update-ai-context.sh new file mode 100755 index 0000000000..1695dd0268 --- /dev/null +++ b/.claude/hooks/update-ai-context.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# SessionStart hook: fetch+rebase ai-workflows so the AI agent +# always has the latest skills and workflow phases. + +fetch_and_rebase() { + local dir="$1" name="$2" + [[ -d "$dir" ]] || return 0 + + if ! git -C "$dir" fetch origin -q 2>/dev/null; then + echo "$name: fetch failed" + return 0 + fi + + local head_before head_after + head_before="$(git -C "$dir" rev-parse HEAD)" + + trap 'git -C "$dir" rebase --abort 2>/dev/null || true' INT TERM + + if git -C "$dir" rebase origin/main --autostash -q >/dev/null 2>&1; then + head_after="$(git -C "$dir" rev-parse HEAD)" + if [[ "$head_before" == "$head_after" ]]; then + echo "$name: up to date" + else + echo "$name: updated" + fi + else + git -C "$dir" rebase --abort 2>/dev/null || true + echo "$name: rebase conflict, skipped" + fi + trap - INT TERM +} + +fetch_and_rebase "${HOME}/.ai-workflows" "ai-workflows" diff --git a/.claude/rules/architecture-patterns.md b/.claude/rules/architecture-patterns.md new file mode 100644 index 0000000000..a4eb14d925 --- /dev/null +++ b/.claude/rules/architecture-patterns.md @@ -0,0 +1,58 @@ +# Architecture Patterns + +## Multi-tenancy + +Tenant-scoped resources include tenant isolation metadata: +- `metadata.annotations["osac.openshift.io/tenant"]` for tenant scoping +- `metadata.annotations["osac.openshift.io/owner-reference"]` for resource hierarchy +- OPA policies enforce isolation at runtime +- Never skip tenant isolation metadata in new tenant-scoped resources +- Use annotations for owner references, not separate fields +- Provider-defined resources (NetworkClass, ExternalIPPool) are exempt — tenants do not interact with them directly + +## Resource Hierarchy + +```text +Cluster Resources: + ClusterOrder → provisions OpenShift clusters via Hosted Control Planes + +Compute Resources: + ComputeInstance → KubeVirt VM, attached to Subnets + SecurityGroups + +Networking Resources: + NetworkClass (platform-defined, read-only for tenants) + └── VirtualNetwork (tenant L2 network with CIDR) + ├── Subnet (CIDR range within VirtualNetwork) + ├── SecurityGroup (firewall rules scoped to VirtualNetwork) + └── NATGateway (SNATs egress traffic through an ExternalIP) + +External IP Resources: + ExternalIPPool (platform-defined, external IP ranges) + ├── ExternalIP (allocated from pool) + └── ExternalIPAttachment (binds ExternalIP to ComputeInstance) + +Tenant Resources: + Tenant → namespace and resource isolation +``` + +Parent-child relationships use owner reference annotations (`osac.openshift.io/owner-reference`). + +## Service Stack (fulfillment-service) + +- PostgreSQL for persistent storage +- gRPC with grpc-gateway for REST/JSON support +- Controller-runtime for Kubernetes integration +- OPA for authorization policies +- Prometheus for metrics + +## Integration Testing (fulfillment-service) + +- Kind clusters (named "fulfillment-service-it") +- TLS with SNI routing via Envoy Gateway +- Keycloak for authentication +- Requires `/etc/hosts` entries: + - `127.0.0.1 keycloak.keycloak.svc.cluster.local` + - `127.0.0.1 fulfillment-api.osac.svc.cluster.local` + - `127.0.0.1 fulfillment-internal-api.osac.svc.cluster.local` +- Use `IT_KEEP_KIND=true` to preserve cluster for debugging +- Clean up with: `kind delete cluster --name fulfillment-service-it` diff --git a/.claude/rules/cli-ux.md b/.claude/rules/cli-ux.md new file mode 100644 index 0000000000..c4fb8e932f --- /dev/null +++ b/.claude/rules/cli-ux.md @@ -0,0 +1,20 @@ +# CLI UX Guidelines + +The OSAC CLI (`fulfillment-service/internal/cmd/cli/`) is a tenant-facing tool for managing infrastructure resources. It is NOT a Kubernetes admin tool - end users should not need to know k8s. + +## Design Inspiration + +- **Primary reference**: kubectl - OSAC APIs are modeled after k8s, so the CLI naturally follows kubectl patterns +- **Per-feature reference**: when implementing new commands, also look at cloud CLIs (AWS, GCP, Rafay, VCD) for inspiration on how they solved the same problem +- The goal is not to copy any single CLI, but to use kubectl as the default and evaluate alternatives per-feature + +## Existing Verbs + +`create`, `get`, `describe`, `edit`, `delete`, `annotate`, `label`, `login`, `logout`, `lookup`, `console`, `whoami`, `version` + +## When Adding New CLI Commands + +- Check if kubectl has an equivalent command and follow its UX patterns +- If kubectl doesn't have one, survey cloud CLIs (az, gcloud, aws) for the closest equivalent +- Keep commands non-interactive and scriptable by default +- All new commands must work without assuming k8s knowledge from the user diff --git a/.claude/rules/networking-design-alignment.md b/.claude/rules/networking-design-alignment.md new file mode 100644 index 0000000000..4f08da4273 --- /dev/null +++ b/.claude/rules/networking-design-alignment.md @@ -0,0 +1,3 @@ +# Networking Design Alignment + +When working on networking-related tasks (Connectivity&Fabric component, networking resources like VirtualNetwork/Subnet/SecurityGroup/ExternalIP/NATGateway/NetworkClass, NICs, ports, interfaces, network attachments, VLANs, fabric/K8s managers, dispatcher, MetalLB, DNS, CIDR, DNAT/SNAT, peering, metering/QoS), read `.design/context/networking-decisions.md` before planning, implementing, or reviewing. For deep context, read the full designs linked from that file. diff --git a/.claude/rules/request-path-tracing.md b/.claude/rules/request-path-tracing.md new file mode 100644 index 0000000000..da94ceb0d4 --- /dev/null +++ b/.claude/rules/request-path-tracing.md @@ -0,0 +1,17 @@ +# Request Path Tracing + +When designing or implementing features triggered by external requests (API endpoints, CLI commands), trace the request path from the user-facing entry point to the handler before writing code. List each layer the request passes through (REST gateway, gRPC interceptors, middleware, server). Any layer that transforms, filters, or routes the request is an affected component - even if its code doesn't change. + +A layer that silently drops or ignores the feature's input is a bug that unit tests at the handler level won't catch. + +## OSAC Example + +The fulfillment-service REST gateway uses grpc-gateway with `DefaultHeaderMatcher`, which only forwards permanent HTTP headers and `Grpc-Metadata-*` prefixed headers to gRPC metadata. Custom HTTP headers are silently dropped. When a feature relies on a custom header reaching the gRPC server, the gateway's header matcher configuration must be checked and updated. + +Request path for REST API calls: + +```text +HTTP client → REST gateway (grpc-gateway mux) → gRPC interceptors → server handler +``` + +Check `fulfillment-service/internal/cmd/service/start/restgateway/start_rest_gateway_cmd.go` for gateway configuration when adding new headers. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..0a41274885 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,43 @@ +{ + "permissions": { + "allow": [ + "Bash(date:*)", + "Bash(echo:*)", + "Bash(cat:*)", + "Bash(ls:*)", + "Bash(mkdir:*)", + "Bash(wc:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(sort:*)", + "Bash(grep:*)", + "Bash(tr:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git tag:*)", + "Bash(jira issue list:*)", + "Bash(jira issue view:*)" + ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PROJECT_DIR}/.claude/hooks/update-ai-context.sh\"", + "timeout": 20 + } + ] + } + ] + }, + "statusLine": { + "type": "command", + "command": "input=$(cat); command -v jq >/dev/null || exit 0; dir=$(printf '%s' \"$input\" | jq -r '.workspace.project_dir'); [ -n \"$dir\" ] || exit 0; printf '%s' \"$input\" | bash \"$dir/.claude/hooks/statusline.sh\" \"$dir\"" + }, + "enabledPlugins": {} +} diff --git a/.gitignore b/.gitignore index f3883faa25..9d2dea141f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ +# AI workflows and skills (installed by tools/bootstrap.sh) +skills/ +.ai-workflows/ +.claude/skills +.claude/commands +.claude/agents +.cursor/ +.gemini/ + # Binaries for programs and plugins *.exe *.exe~ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..5c93370451 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# OSAC Mono-Repo + +OSAC (Open Sovereign AI Cloud) is a fulfillment system for provisioning Kubernetes clusters, compute instances, bare-metal hosts, and networking. This mono-repo contains six components, each with its own `CLAUDE.md`/`AGENTS.md` — **read the component's docs before making changes in it**. + +## Components + +| Component | Description | +|-----------|-------------| +| `fulfillment-service/` | gRPC/REST API server + `osac` CLI (PostgreSQL, OPA) | +| `osac-operator/` | Kubernetes operator for CRDs (ClusterOrder, ComputeInstance, Tenant, networking) | +| `osac-aap/` | Ansible playbooks for infrastructure provisioning | +| `osac-installer/` | Helm-based three-phase deployment orchestrator | +| `bare-metal-fulfillment-operator/` | Kubernetes operator for bare-metal host pools | +| `osac-csi-driver/` | CSI meta-driver aggregating vendor storage drivers | + +## External Repos + +Clone as siblings for cross-repo workflows: + +| Repo | Description | +|------|-------------| +| [osac-test-infra](https://github.com/osac-project/osac-test-infra) | E2E pytest tests against the fulfillment-service gRPC API | +| [osac-ui](https://github.com/osac-project/osac-ui) | Web console (React, PatternFly 6) | +| [enhancement-proposals](https://github.com/osac-project/enhancement-proposals) | PRDs and design documents (two-stage EP flow) | +| [docs](https://github.com/osac-project/docs) | Architecture docs and guides | + +## Cross-Component Changes + +A feature spanning multiple components lands in a single branch and PR. Apply changes in dependency order: fulfillment-service (proto) → osac-operator (CRDs, controllers) → osac-aap (playbooks) → osac-installer (RBAC, Helm). For deployment coordination (image tags, submodules), see `osac-installer/AGENTS.md`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/tools/bootstrap.sh b/tools/bootstrap.sh new file mode 100755 index 0000000000..9028b122eb --- /dev/null +++ b/tools/bootstrap.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Install AI workflow skills for this repo. +# +# Usage: tools/bootstrap.sh +# +# This script: +# 1. Clones or updates flightctl/ai-workflows (prefers ~/.ai-workflows) +# 2. Installs workflows (bugfix, implement, prd, design, e2e) +# +# Re-run anytime to update to latest main. +set -euo pipefail + +SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +PROJECT_ROOT="$(realpath "${SCRIPT_DIR}/..")" + +AI_WORKFLOWS_REPO="flightctl/ai-workflows" +AI_WORKFLOWS_DIR="" + +# --- ai-workflows (flightctl) --- + +if [[ -d "${HOME}/.ai-workflows" ]]; then + AI_WORKFLOWS_DIR="$(readlink -f "${HOME}/.ai-workflows")" + echo "Updating ai-workflows (${AI_WORKFLOWS_DIR})..." + if ! (cd "$AI_WORKFLOWS_DIR" && git fetch origin -q); then + echo " Fetch failed for ai-workflows. Skipping update." + elif ! (cd "$AI_WORKFLOWS_DIR" && git rebase origin/main --autostash -q); then + (cd "$AI_WORKFLOWS_DIR" && git rebase --abort 2>/dev/null || true) + echo " Rebase failed for ai-workflows. Resolve manually: cd $AI_WORKFLOWS_DIR && git rebase origin/main" + else + echo " ai-workflows up to date" + fi +elif [[ -d "${PROJECT_ROOT}/.ai-workflows" ]]; then + AI_WORKFLOWS_DIR="${PROJECT_ROOT}/.ai-workflows" + echo "Updating ai-workflows (.ai-workflows)..." + if ! (cd "$AI_WORKFLOWS_DIR" && git fetch origin -q); then + echo " Fetch failed. Skipping update." + elif ! (cd "$AI_WORKFLOWS_DIR" && git rebase origin/main --autostash -q); then + (cd "$AI_WORKFLOWS_DIR" && git rebase --abort 2>/dev/null || true) + echo " Rebase failed. Resolve manually: cd $AI_WORKFLOWS_DIR && git rebase origin/main" + else + echo " ai-workflows up to date" + fi +else + AI_WORKFLOWS_DIR="${PROJECT_ROOT}/.ai-workflows" + echo "Cloning ai-workflows..." + git clone "https://github.com/${AI_WORKFLOWS_REPO}.git" "${AI_WORKFLOWS_DIR}" +fi + +echo "Installing ai-workflows skills..." +AI_WORKFLOWS="bugfix,implement,prd,design,e2e" +"${AI_WORKFLOWS_DIR}/install.sh" all --project "${PROJECT_ROOT}" --workflows "${AI_WORKFLOWS}" + +echo "" +echo "AI workflows installed."