diff --git a/.env.example b/.env.example
index 839f4c9d2..2e508af1e 100644
--- a/.env.example
+++ b/.env.example
@@ -52,12 +52,12 @@ RENEW_DEADLINE=40 # Seconds; must be < LEASE_DURATION
RETRY_PERIOD=10 # Seconds; retry interval for leader election
# ------------------------------------------------------------------------------
-# Catalog / Stack Configuration
+# Blueprint / Stack Configuration
# ------------------------------------------------------------------------------
-# Paths to Catalog or Stack files (comma-separated or single path)
+# Paths to Blueprint or Stack files (comma-separated or single path)
# Can be overridden with --file flag
-CATALOG_PATH=./catalog.yaml
+BLUEPRINT_PATH=./blueprint.yaml
# Default queue depth and degrade threshold for CRDs
MAX_QUEUE_DEPTH=100
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1387382f6..37f3bb152 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -31,4 +31,4 @@ What actually happened.
## Additional Context
-Logs, Catalog snippets, CRDs, etc.
+Logs, Blueprint snippets, CRDs, etc.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index c743403b8..d0325f70f 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -14,7 +14,7 @@ Why is this useful? What problem does it solve?
## Proposal
-How you imagine it working (CLI, Catalog, Stack, Registry, etc.).
+How you imagine it working (CLI, Blueprint, Stack, Registry, etc.).
## Alternatives
diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml
index af6cb643e..8f5e33e5f 100644
--- a/.github/workflows/build-matrix.yml
+++ b/.github/workflows/build-matrix.yml
@@ -47,8 +47,8 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Run note generator
- run: make generate-notes
+ - name: Run function generator
+ run: make generate-functions
- name: Cache compiled binaries for ${{ matrix.os }}_${{ matrix.arch }}
id: cache-binaries
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 24faba366..6a33d05b9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,48 +1,66 @@
+# .github/workflows/ci.yml
name: CI
on:
- # push:
- # branches: [main]
+ workflow_dispatch:
+ push:
+ branches: [main]
+ paths:
+ - 'cmd/**'
+ - 'pkg/**'
+ - 'domain/**'
+ - 'examples/**'
+ - 'go.mod'
+ - 'go.sum'
pull_request:
- branches: [disabled]
+ types: [opened, synchronize, reopened]
+ paths:
+ - 'cmd/**'
+ - 'pkg/**'
+ - 'domain/**'
+ - 'examples/**'
+ - 'go.mod'
+ - 'go.sum'
jobs:
- # ── Lint ──────────────────────────────────────────────────────────────────
- lint:
- name: Lint
+ validate-functions:
+ name: Validate generated functions
runs-on: ubuntu-latest
-
steps:
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
- # - name: Set up Go
- # uses: actions/setup-go@v5
- # with:
- # go-version-file: go.mod
- # cache: true
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
- # - name: golangci-lint
- # uses: golangci/golangci-lint-action@v6
- # with:
- # version: latest
+ - name: Run function generator
+ run: make generate-functions
- - name: Write summary
- if: always()
+ - name: Check for uncommitted changes (generated function registries)
run: |
- echo "## Lint" >> "$GITHUB_STEP_SUMMARY"
- echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY"
- echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
- echo "| golangci-lint | ${{ job.status == 'success' && '✅ passed' || '❌ failed' }} |" >> "$GITHUB_STEP_SUMMARY"
-
- # ── Build ──────────────────────────────────────────────────────────────────
- build:
- name: Build
- runs-on: ubuntu-latest
+ FUNCS="pkg/template/funcs/funcs_generated.go docs/reference/inrun-functions"
+ if ! git diff --quiet -- $FUNCS; then
+ echo "❌ Generated function files are out of date:"
+ git diff --stat -- $FUNCS
+ echo "Run 'make generate-functions' locally and commit the changes."
+ exit 1
+ else
+ echo "✅ Function list is up-to-date."
+ fi
+ validate-resource-docs:
+ name: Validate generated resource docs
+ runs-on: ubuntu-latest
steps:
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
@@ -50,92 +68,44 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Build
- run: go build ./...
+ - name: Run resource docs generator
+ run: make generate-resource-docs
- - name: Build CLI binary
+ - name: Check for uncommitted changes (docs/reference/schema/resources)
run: |
- go build -o ./bin/inrun ./cmd/inrun/
- echo "✅ Binary built:"
- ls -lh ./bin/inrun
+ if ! git diff --quiet docs/reference/schema/resources/; then
+ echo "❌ docs/reference/schema/resources/ is out of date."
+ echo "Run 'make generate-resource-docs' locally and commit the changes."
+ exit 1
+ else
+ echo "✅ Resource docs are up-to-date."
+ fi
- - name: Write summary
- if: always()
- run: |
- echo "## Build" >> "$GITHUB_STEP_SUMMARY"
- echo "| Step | Status |" >> "$GITHUB_STEP_SUMMARY"
- echo "|------|--------|" >> "$GITHUB_STEP_SUMMARY"
- echo "| go build ./... | ${{ job.status == 'success' && '✅ passed' || '❌ failed' }} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| CLI binary | ${{ job.status == 'success' && '✅ built' || '❌ failed' }} |" >> "$GITHUB_STEP_SUMMARY"
-
- # ── Test ───────────────────────────────────────────────────────────────────
- test:
- name: Test
+ validate-code:
+ name: Run static checks
runs-on: ubuntu-latest
-
steps:
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@v6
- # - name: Set up Go
- # uses: actions/setup-go@v5
- # with:
- # go-version-file: go.mod
- # cache: true
-
- - name: Run tests
- run: |
- go test ./... \
- -v \
- -race \
- -coverprofile=coverage.out \
- -covermode=atomic \
- -timeout 120s \
- 2>&1 | tee test_output.txt
-
- - name: Vulnerability scan
- run: |
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck ./...
-
- - name: Coverage report
- run: |
- go tool cover -func=coverage.out | tail -1
- COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
- echo "COVERAGE=${COVERAGE}" >> "$GITHUB_ENV"
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
- - name: Write summary
- if: always()
- run: |
- echo "## Tests" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
-
- # Count pass/fail
- PASSED=$(grep -c "^--- PASS" test_output.txt 2>/dev/null || echo 0)
- FAILED=$(grep -c "^--- FAIL" test_output.txt 2>/dev/null || echo 0)
-
- echo "| Result | Count |" >> "$GITHUB_STEP_SUMMARY"
- echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
- echo "| ✅ Passed | ${PASSED} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| ❌ Failed | ${FAILED} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| 📊 Coverage | ${COVERAGE:-n/a} |" >> "$GITHUB_STEP_SUMMARY"
-
- if [ "${FAILED}" -gt 0 ]; then
- echo "" >> "$GITHUB_STEP_SUMMARY"
- echo "### Failed tests" >> "$GITHUB_STEP_SUMMARY"
- echo '```' >> "$GITHUB_STEP_SUMMARY"
- grep "^--- FAIL" test_output.txt >> "$GITHUB_STEP_SUMMARY" || true
- echo '```' >> "$GITHUB_STEP_SUMMARY"
- fi
+ - name: Run tests (no cluster required)
+ run: make test
- # ── Validate examples ──────────────────────────────────────────────────────
- validate-examples:
- name: Validate examples
+ # Runs every example's simulate.yaml against the CLI built from this
+ # checkout, the way each example's README tells users to. No cluster.
+ # Typed examples are skipped: each builds its own binary from a nested
+ # module, which needs a published Inrun version.
+ simulate-examples:
+ name: Simulate examples
runs-on: ubuntu-latest
- needs: build
-
steps:
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@v6
- name: Set up Go
@@ -144,38 +114,47 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Build CLI
- run: go build -o ./bin/inrun ./cmd/inrun/
+ - name: Build inrun
+ run: go build -o "$RUNNER_TEMP/inrun" ./cmd/inrun
- - name: Validate examples
+ - name: Start the remote example's reconciler
+ working-directory: examples/reconcilers/remote/endpoint
+ env:
+ RECONCILER_TOKEN: inrun-demo-token
run: |
- for k in $(find examples -name catalog.yaml | sort); do
- ./bin/inrun validate --file "$k"
+ python3 reconciler/main.py > "$RUNNER_TEMP/reconciler.log" 2>&1 &
+ for i in $(seq 30); do
+ curl -s -o /dev/null localhost:8025 && exit 0
+ sleep 1
done
+ echo "reconciler did not start"; cat "$RUNNER_TEMP/reconciler.log"; exit 1
- # ── Summary ────────────────────────────────────────────────────────────────
- summary:
- name: CI Summary
- runs-on: ubuntu-latest
- needs: [lint, build, test, validate-examples]
- if: always()
+ - name: Simulate declarative and remote examples
+ working-directory: examples
+ run: |
+ "$RUNNER_TEMP/inrun" simulate ./... --skip typed,intent --dev-server
- steps:
- - name: Write final summary
+ - name: Simulate the intent example
+ working-directory: examples/intent
run: |
- echo "## CI Summary" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
- echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
- echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"
- echo "| Lint | ${{ needs.lint.result == 'success' && '✅' || '❌' }} ${{ needs.lint.result }} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| Build | ${{ needs.build.result == 'success' && '✅' || '❌' }} ${{ needs.build.result }} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| Test | ${{ needs.test.result == 'success' && '✅' || '❌' }} ${{ needs.test.result }} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| Validate examples | ${{ needs.validate-examples.result == 'success' && '✅' || '❌' }} ${{ needs.validate-examples.result }} |" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
-
- ALL_PASSED="${{ needs.lint.result == 'success' && needs.build.result == 'success' && needs.test.result == 'success' && needs.validate-examples.result == 'success' }}"
- if [ "$ALL_PASSED" = "true" ]; then
- echo "### ✅ All checks passed — ready to merge" >> "$GITHUB_STEP_SUMMARY"
- else
- echo "### ❌ Some checks failed — review above" >> "$GITHUB_STEP_SUMMARY"
- fi
\ No newline at end of file
+ "$RUNNER_TEMP/inrun" serve play -i intent.json --token dev --simulate=test/simulate.yaml
+
+ vuln-check:
+ name: Vulnerability check (${{ matrix.target }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ target: [vuln-console, vuln-runtime, vuln-gateway]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
+
+ - name: Run vulnerability check
+ run: make ${{ matrix.target }}
diff --git a/.github/workflows/package-examples.yml b/.github/workflows/package-examples.yml
index d25bbbeee..2e42584eb 100644
--- a/.github/workflows/package-examples.yml
+++ b/.github/workflows/package-examples.yml
@@ -27,7 +27,7 @@ jobs:
mkdir -p dist/examples
TAG="${{ github.ref_name }}"
- for pack in reconcilers/declarative reconcilers/remote reconcilers/typed intent; do
+ for pack in reconcilers/declarative reconcilers/remote reconcilers/typed intent migration registry; do
tar -czf "dist/examples_$(basename $pack)_${TAG}.tar.gz" -C "examples/$pack" .
done
@@ -49,10 +49,12 @@ jobs:
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Pack | Description | Archive |" >> "$GITHUB_STEP_SUMMARY"
echo "|------|-------------|---------|" >> "$GITHUB_STEP_SUMMARY"
- echo "| Declarative | Operators declared in a Catalog | \`examples_declarative_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Declarative | Operators declared in a Blueprint | \`examples_declarative_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Remote | A reconciler in any language over HTTP | \`examples_remote_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Typed | Go hooks, constructor, controller-runtime drop-in | \`examples_typed_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Intent | Intent in through the gateway, view out | \`examples_intent_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Migration | Change the CRD under live CRs | \`examples_migration_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Registry | Consume, publish and deprecate patterns | \`examples_registry_${TAG}.tar.gz\` |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Download from the release assets or run:**" >> "$GITHUB_STEP_SUMMARY"
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/release-summary.yml b/.github/workflows/release-summary.yml
index dcd650ce6..3005fb2dd 100644
--- a/.github/workflows/release-summary.yml
+++ b/.github/workflows/release-summary.yml
@@ -86,7 +86,7 @@ jobs:
echo "| Console binaries | $(icon ${{ inputs.build_result }}) ${{ inputs.build_result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Example packs | $(icon ${{ inputs.examples_result }}) ${{ inputs.examples_result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Container images | $(icon ${{ inputs.image_result }}) ${{ inputs.image_result }} |" >> "$GITHUB_STEP_SUMMARY"
- echo "| GPG signing + GitHub Release | $(icon ${{ inputs.release_result }}) ${{ inputs.release_result }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Keyless signing + GitHub Release | $(icon ${{ inputs.release_result }}) ${{ inputs.release_result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Helm chart | $(icon ${{ inputs.helm_result }}) ${{ inputs.helm_result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Homebrew formulas | $(icon ${{ inputs.homebrew_result }}) ${{ inputs.homebrew_result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Windows packages | 📦 Download from [GitHub Releases](https://github.com/${REPO}/releases/tag/${TAG}) |" >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7d57ab40e..1e888586e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -57,6 +57,9 @@ jobs:
release:
name: Sign and Release
uses: ./.github/workflows/sign-and-release.yml
+ permissions:
+ contents: write
+ id-token: write
needs: [prepare, build, image, examples]
secrets: inherit
with:
diff --git a/.github/workflows/sign-and-release.yml b/.github/workflows/sign-and-release.yml
index 9cec1187a..01a97ab79 100644
--- a/.github/workflows/sign-and-release.yml
+++ b/.github/workflows/sign-and-release.yml
@@ -57,6 +57,9 @@ jobs:
release:
name: Sign and Release
runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -69,55 +72,32 @@ jobs:
path: dist/
merge-multiple: true
- - name: Import GPG signing key
- env:
- GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- run: |
- echo "${GPG_PRIVATE_KEY}" | gpg --batch --import
- FINGERPRINT=$(gpg --list-secret-keys --with-colons releases@${{ inputs.inrun }}.io \
- | grep '^fpr' \
- | head -1 \
- | cut -d: -f10)
- echo "${FINGERPRINT}:6:" | gpg --import-ownertrust
- echo "✅ GPG key imported: ${FINGERPRINT}"
-
- - name: Sign tarballs (and ZIPs if present)
- env:
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- run: |
- cd dist
- for archive in inrun_*.tar.gz inrun-console_*.tar.gz; do
- gpg --batch --yes --passphrase "${GPG_PASSPHRASE}" --pinentry-mode loopback --armor --detach-sign "${archive}"
- echo "✅ Signed: ${archive}"
- done
- for zip in inrun_*.zip inrun-console_*.zip; do
- [ -f "$zip" ] || continue
- gpg --batch --yes --passphrase "${GPG_PASSPHRASE}" --pinentry-mode loopback --armor --detach-sign "${zip}"
- echo "✅ Signed: ${zip}"
- done
-
- - name: Export public key
- run: |
- gpg --armor --export releases@${{ inputs.inrun }}.io > dist/${{ inputs.inrun }}-public-key.asc
- echo "✅ Public key exported"
+ - name: Install cosign
+ uses: sigstore/cosign-installer@v4
- name: Generate checksums
run: |
cd dist
- sha256sum inrun_*.tar.gz inrun-console_*.tar.gz inrun_*.zip inrun-console_*.zip 2>/dev/null | grep -v "No such file" > checksums.txt
+ sha256sum inrun_*.tar.gz inrun-console_*.tar.gz inrun_*.zip inrun-console_*.zip examples_*.tar.gz 2>/dev/null | grep -v "No such file" > checksums.txt
echo "✅ Checksums generated"
- - name: Verify signatures
+ # Keyless: cosign signs with this workflow's GitHub OIDC identity, so
+ # there is no key to keep. The bundle carries the signature and the
+ # certificate; every archive is covered through checksums.txt.
+ - name: Sign checksums
+ run: |
+ cd dist
+ cosign sign-blob --yes --bundle checksums.txt.sigstore.json checksums.txt
+ echo "✅ Signed: checksums.txt"
+
+ - name: Verify signature and checksums
run: |
cd dist
- for archive in inrun_*.tar.gz inrun-console_*.tar.gz; do
- gpg --verify "${archive}.asc" "${archive}" && echo "✅ Verified: ${archive}" || { echo "❌ Failed: ${archive}"; exit 1; }
- done
- for zip in inrun_*.zip inrun-console_*.zip; do
- [ -f "$zip" ] || continue
- gpg --verify "${zip}.asc" "${zip}" && echo "✅ Verified: ${zip}" || { echo "❌ Failed: ${zip}"; exit 1; }
- done
+ cosign verify-blob checksums.txt \
+ --bundle checksums.txt.sigstore.json \
+ --certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/sign-and-release.yml@${{ github.ref }}" \
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com
+ sha256sum -c checksums.txt
- name: Extract changelog
run: |
@@ -185,35 +165,25 @@ jobs:
prerelease: ${{ contains(github.ref_name, '-') }}
files: |
dist/inrun_linux_amd64.tar.gz
- dist/inrun_linux_amd64.tar.gz.asc
dist/inrun_linux_arm64.tar.gz
- dist/inrun_linux_arm64.tar.gz.asc
dist/inrun_darwin_amd64.tar.gz
- dist/inrun_darwin_amd64.tar.gz.asc
dist/inrun_darwin_arm64.tar.gz
- dist/inrun_darwin_arm64.tar.gz.asc
dist/inrun_windows_amd64.tar.gz
- dist/inrun_windows_amd64.tar.gz.asc
dist/inrun_windows_amd64.zip
- dist/inrun_windows_amd64.zip.asc
dist/inrun-console_linux_amd64.tar.gz
- dist/inrun-console_linux_amd64.tar.gz.asc
dist/inrun-console_linux_arm64.tar.gz
- dist/inrun-console_linux_arm64.tar.gz.asc
dist/inrun-console_darwin_amd64.tar.gz
- dist/inrun-console_darwin_amd64.tar.gz.asc
dist/inrun-console_darwin_arm64.tar.gz
- dist/inrun-console_darwin_arm64.tar.gz.asc
dist/inrun-console_windows_amd64.tar.gz
- dist/inrun-console_windows_amd64.tar.gz.asc
dist/inrun-console_windows_amd64.zip
- dist/inrun-console_windows_amd64.zip.asc
dist/checksums.txt
- dist/${{ inputs.inrun }}-public-key.asc
+ dist/checksums.txt.sigstore.json
dist/examples_declarative_${{ github.ref_name }}.tar.gz
dist/examples_remote_${{ github.ref_name }}.tar.gz
dist/examples_typed_${{ github.ref_name }}.tar.gz
dist/examples_intent_${{ github.ref_name }}.tar.gz
+ dist/examples_migration_${{ github.ref_name }}.tar.gz
+ dist/examples_registry_${{ github.ref_name }}.tar.gz
fail_on_unmatched_files: true
- name: Upload checksums as artifact
diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml
deleted file mode 100644
index 9d9feff04..000000000
--- a/.github/workflows/validate-pr.yml
+++ /dev/null
@@ -1,292 +0,0 @@
-# .github/workflows/validate-pr.yml
-name: Validate Pull Request
-
-on:
- workflow_dispatch:
- pull_request:
- types: [opened, synchronize, reopened]
- paths:
- - 'cmd/**'
- - 'pkg/**'
- - 'domain/**'
- - 'examples/**'
- - 'go.mod'
- - 'go.sum'
-
-jobs:
- validate-notes:
- name: Validate generated notes
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
- with:
- fetch-depth: 0
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Run note generator
- run: make generate-notes
-
- - name: Check for uncommitted changes (list_generated.go)
- run: |
- if ! git diff --quiet pkg/note/list_generated.go; then
- echo "❌ pkg/note/list_generated.go is out of date."
- echo "Run 'make generate-notes' locally and commit the changes."
- exit 1
- else
- echo "✅ Note list is up-to-date."
- fi
-
- validate-resource-docs:
- name: Validate generated resource docs
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
- with:
- fetch-depth: 0
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Run resource docs generator
- run: make generate-resource-docs
-
- - name: Check for uncommitted changes (docs/reference/schema/06-resources)
- run: |
- if ! git diff --quiet docs/reference/schema/06-resources/; then
- echo "❌ docs/reference/schema/06-resources/ is out of date."
- echo "Run 'make generate-resource-docs' locally and commit the changes."
- exit 1
- else
- echo "✅ Resource docs are up-to-date."
- fi
-
- validate-code:
- name: Run static checks
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Run tests (no cluster required)
- run: make test
-
- # Runs every example's simulate.yaml against the CLI built from this
- # checkout, the way each example's README tells users to. No cluster.
- # Typed examples are skipped: each builds its own binary from a nested
- # module, which needs a published Inrun version.
- simulate-examples:
- name: Simulate examples
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Build inrun
- run: go build -o "$RUNNER_TEMP/inrun" ./cmd/inrun
-
- - name: Start the remote example's reconciler
- working-directory: examples/reconcilers/remote
- env:
- RECONCILER_TOKEN: inrun-demo-token
- run: |
- python3 reconciler/main.py > "$RUNNER_TEMP/reconciler.log" 2>&1 &
- for i in $(seq 30); do
- curl -s -o /dev/null localhost:8025 && exit 0
- sleep 1
- done
- echo "reconciler did not start"; cat "$RUNNER_TEMP/reconciler.log"; exit 1
-
- - name: Simulate declarative and remote examples
- working-directory: examples
- run: |
- "$RUNNER_TEMP/inrun" simulate ./... --skip typed,intent
-
- - name: Simulate the intent example
- working-directory: examples/intent
- run: |
- "$RUNNER_TEMP/inrun" serve play -i intent.json --token dev --simulate=test/simulate.yaml
-
- # Runs as its own job, in parallel with validate-code, so a slow vuln DB
- # fetch doesn't add to the critical path of the tests everyone's actually
- # waiting on. Matrix so all three targets also run in parallel with each
- # other, and so a failure names exactly which one.
- #
- # Scoped to what actually ships: runtime, gateway, and Console.
- # vuln-inrun scans the dev-CLI build config (no tags — never deployed)
- # and is manually triggered instead, from vuln-inrun.yml — that's also
- # where the known, accepted findings live (see the comment above `vuln` in
- # the Makefile); none of them reach these three, so this is a hard gate —
- # no continue-on-error.
- vuln-check:
- name: Vulnerability check (${{ matrix.target }})
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- target: [vuln-console, vuln-runtime, vuln-gateway]
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Run vulnerability check
- run: make ${{ matrix.target }}
-
- # ── Fixture: note family ────────────────────────────────────────────────────
- # Runs only when the note package or its fixture changes.
- # Exercises every kubernetes-family note against a real API server.
- fixture-note:
- name: Fixture — note (kind cluster)
- runs-on: ubuntu-latest
- if: >
- github.event_name == 'workflow_dispatch' ||
- contains(github.event.pull_request.changed_files, 'pkg/note/')
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Build inrun
- run: make inrun
-
- - name: Create kind cluster
- uses: helm/kind-action@v1
- with:
- cluster_name: inrun-note-fixture
-
- - name: Apply CRD
- run: kubectl apply -f pkg/note/fixture/crd.yaml
-
- - name: Generate and apply bundle
- run: |
- $HOME/.inrun/bin/inrun bundle \
- --file pkg/note/fixture/catalog.yaml | kubectl apply -f -
-
- - name: Install Inrun via Helm
- run: |
- helm install inrun ./charts/inrun \
- --namespace default \
- --wait --timeout 120s
-
- - name: Apply CR
- run: kubectl apply -f pkg/note/fixture/cr.yaml
-
- - name: Wait for status
- run: |
- kubectl wait noteprobe/my-probe \
- --for=jsonpath='{.status.phase}'=Ready \
- --timeout=120s
-
- - name: Assert note fields populated
- run: |
- kubectl get noteprobe my-probe -o yaml | tee /tmp/probe.yaml
- # Fail if phase is empty — means status is not being written
- PHASE=$(kubectl get noteprobe my-probe -o jsonpath='{.status.phase}')
- [ -n "$PHASE" ] || (echo "❌ status.phase is empty"; exit 1)
- echo "✅ Note fixture passed — phase: $PHASE"
-
- - name: Debug on failure
- if: failure()
- run: |
- echo "=== ReconcilerProbe ===" && kubectl get noteprobe my-probe -o yaml || true
- echo "=== Operator logs ===" && kubectl logs -l app.kubernetes.io/name=inrun --tail=100 || true
-
- # ── Fixture: reconciler run_* ───────────────────────────────────────────────
- # Runs only when the reconciler package or its fixture changes.
- # Exercises every run_* resource type in a single reconcile cycle.
- fixture-reconciler:
- name: Fixture — reconciler run_* (kind cluster)
- runs-on: ubuntu-latest
- if: >
- github.event_name == 'workflow_dispatch' ||
- contains(github.event.pull_request.changed_files, 'pkg/runtime/reconciler/')
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: true
-
- - name: Build inrun
- run: make inrun
-
- - name: Create kind cluster
- uses: helm/kind-action@v1
- with:
- cluster_name: inrun-reconciler-fixture
-
- - name: Apply CRD
- run: kubectl apply -f pkg/runtime/reconciler/fixture/crd.yaml
-
- - name: Generate and apply bundle
- run: |
- $HOME/.inrun/bin/inrun bundle \
- --file pkg/runtime/reconciler/fixture/catalog.yaml | kubectl apply -f -
-
- - name: Install Inrun via Helm
- run: |
- helm install inrun ./charts/inrun \
- --namespace default \
- --wait --timeout 120s
-
- - name: Apply CR
- run: kubectl apply -f pkg/runtime/reconciler/fixture/cr.yaml
-
- - name: Wait for status
- run: |
- kubectl wait reconcilerprobe/probe \
- --for=jsonpath='{.status.tier}'=premium \
- --timeout=120s
-
- - name: Assert all run_* resources created
- run: |
- kubectl get deployment probe-app || (echo "❌ probe-app deployment missing"; exit 1)
- kubectl get deployment probe-premium || (echo "❌ probe-premium deployment missing"; exit 1)
- kubectl get service probe-svc || (echo "❌ probe-svc service missing"; exit 1)
- kubectl get configmap probe-config || (echo "❌ probe-config configmap missing"; exit 1)
- kubectl get secret probe-creds || (echo "❌ probe-creds secret missing"; exit 1)
- kubectl get serviceaccount probe-sa || (echo "❌ probe-sa serviceaccount missing"; exit 1)
- kubectl get cronjob probe-cron || (echo "❌ probe-cron cronjob missing"; exit 1)
- echo "✅ Reconciler fixture passed — all run_* resources present"
-
- - name: Debug on failure
- if: failure()
- run: |
- echo "=== ReconcilerProbe ===" && kubectl get reconcilerprobe probe -o yaml || true
- echo "=== All resources in default ===" && kubectl get all || true
- echo "=== Operator logs ===" && kubectl logs -l app.kubernetes.io/name=inrun --tail=100 || true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73d16eba1..d1c92a484 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,94 +2,10 @@
Inrun follows [Semantic Versioning](https://semver.org/). Until `v1.0`, minor versions may contain breaking changes.
-## Unreleased
+## v0.1.0
-Inrun is the intent runner that began as Orkestra. This first release narrows it to one loop: an intent arrives through the gateway, becomes a custom resource, is reconciled, and returns to the caller as a view.
+Orkestra becomes Inrun, and versions start again at `v0.1.0`.
-### Highlights
+Everything Orkestra named is renamed: the CLI is `inrun`, the API group, labels, annotations and finalizers are `inrun.dev`, and Katalog, Komposer and Motif are Blueprint, Stack and Module. Resources created by Orkestra are not adopted.
-- **One reconciler contract.** Hooks, constructors and remote reconcilers now receive the same request and return the same result: status, resources, labels and annotations, a requeue, or a cleanup. Inrun performs every write and checks each resource against the Catalog's `managedResources` first. Declarative reconcilers keep creating resources from their templates.
-- **Args in the reconciler's own vocabulary.** `reconcile.args` maps fields of the custom resource onto the names your reconciler uses, rendered on every reconcile. `reconcile.external` calls run beforehand, so args can carry their results.
-- **Cleanup from code.** A reconciler can ask for its resource to be deleted after a grace period. Requeues and cleanup are independent; whichever is due first acts, and the reconciler keeps running in between.
-- **Typed Catalogs through the stock gateway.** The gateway no longer needs compiled types, so one gateway image serves any Catalog.
-
-### Upgrading from Orkestra
-
-Resources created by Orkestra are not adopted. Rename as follows, then
-redeploy.
-
-| Orkestra | Inrun |
-| --- | --- |
-| Go module, CLI, console | `github.com/intentd/inrun`, `inrun`, `inrun-console` |
-| Images | `ghcr.io/intentd/inrun`, `inrun-gateway`, `inrun-console` |
-| Helm chart | `inrun`, starting at `0.1.0` |
-| API group, labels, annotations, finalizers | `inrun.dev` |
-| Environment variables | `INRUN_*` |
-| `Katalog`, `Komposer`, `Motif` | `Catalog`, `Stack`, `Module` |
-| `inrun run` | `inrun` with no subcommand |
-| Control Center | `inrun console` |
-| `hooks.args`, `constructor.args`, `remote.args` | `reconcile.args` |
-| `managedResources` under `hooks`, `constructor` or `remote` | `reconcile.managedResources` |
-| `hooks.external` | `reconcile.external` |
-| `reconcile.remote.payload` | `reconcile.remote.request` |
-| `runtime.cleanup.deleteAfter` | `runtime.cleanup.after` |
-
-**Typed reconcilers (Go):**
-
-- Hooks have a new signature: `OnReconcile(ctx, req, obj) (domain.Result, error)`, and the same for
- `OnDelete`. `OnNotFound` is removed.
-- Read args from `req.Args`. `kube.Args()`, `WithArgs` and `ScopedFor` are removed; `kubeclient.Args` is now `domain.Args`, and `BindArgs` is `Bind`.
-- `domain.Result.StatusPatch` is now `Status`.
-
-**Remote reconcilers:**
-
-- `result` is `ok` or `error`. Ask for a requeue with the `requeue` field (`{"after": "60s", "reason": "…"}`) or the `requeueAfter` shorthand.
-- Return resources instead of applying them. Inrun applies them under the field manager `inrun-reconciler`, which was `inrun-remote`.
-- The request body is `key`, `apiVersion`, `kind`, `target`, `object`, `args` and `children`. `gvk` is replaced by `apiVersion` and `kind`, `children` moves up from `prepared`, and `prepared` is gone: `object` is the only copy of the CR, so `request.object.exclude` hides what it names.
-
-**Helm:** point `catalog.existingConfigMap` at the ConfigMap from `inrun generate bundle`. The inline Catalog values are removed.
-
-### Added
-
-- `inrun` also starts the console on :8081, and the gateway on :8082 when the Catalog serves a CRD, so one command runs the whole loop locally; the console can create resources through that gateway. Set `INRUN_CONSOLE_PORT` or `INRUN_GATEWAY_PORT` to move them. Either one failing to start is logged; the runtime runs regardless.
-- The console honours `--log-level`; it printed every request and fetch.
-- The gateway's `/api/v1/apply` takes YAML as well as JSON, like an intent file for `inrun serve play`. Both decode to the same values, so an intent builds the same CR in either format.
-- A Simulate spec can name an intent instead of a CR (`intent`, with the `token` it is sent with): `inrun simulate` builds the CR the way the gateway does, then reconciles it. `inrun serve play` takes `--envtest`, and `--envtest` installs the Catalog's `crdFile`s when a spec lists no CRDs.
-- Reconcilers can return labels and annotations for their resource. Inrun applies them with server-side apply: a key the reconciler stops returning is removed, and keys set by anyone else are left alone. The `inrun.dev` domain is reserved.
-- Requeues carry a reason, shown in the logs.
-- Typed reconcilers can return resources, in full or intent form.
-- The CR lifecycle moves from the generic reconciler into the runtime, so every reconciler gets it: the cleanup finalizer and a finalizing reconcile (`req.Finalizing()`; remote: `"finalizing": true`), deletion of children owner references cannot reach, the serve-target sweep, and `Reconciled`/`ReconcileError` events.
-- `runtime.finalizers.timeout` releases a CR still finalizing after that long, with a `FinalizeTimeout` warning.
-- `reconcile.prune` deletes children a reconciler applied before and no longer returns. It is off unless `prune.enabled` is true, and a serve target can set its own. Only children labelled with the CR's UID (`inrun.dev/owner-uid`) and applied by `inrun-reconciler` are candidates; `prune.keep` spares kinds such as PersistentVolumeClaims, an empty Result prunes nothing unless `prune.allowEmpty` is set, and nothing is pruned when the reconcile or an apply fails. Each deletion emits a `Pruned` event.
-- Intent form covers `namespace` and `pdb`, besides `deployment`, `statefulset`, `service`, `ingress`, `configmap`, `secret`, `serviceaccount`, `job`, `cronjob` and `custom`.
-- `inrun simulate` runs remote reconcilers against their endpoint.
-- `inrun e2e --chart` installs a local chart, to test unreleased changes end to end.
-- `inrun generate bundle`, `configmap` and `rbac` accept Catalogs that use `crdFile`.
-- One project layout: `catalog.yaml` at the root, manifests in `manifests/`, and tests in `test/`. `inrun init`, `create pattern` and `migrate` write it, and `inrun simulate` and `inrun e2e` read it by default.
-- Examples in four packs: declarative, remote, typed and intent.
-
-### Changed
-
-- `reconcile.args`, `reconcile.managedResources` and `reconcile.external` are rejected on declarative-only CRDs.
-- Constructor args are rendered as templates, like hook args.
-- A serve target's `args` merge over the CRD's; its `managedResources` and `external` replace them.
-
-### Removed
-
-- `inrun validate --play`: a Simulate spec that gets its CR from an intent says so with `spec.intent`.
-- Features that don't serve the intent loop. They remain on the `archive` branch.
-
-### Fixed
-
-- A reconciler that returns resources was called again after every reconcile: each apply stamped the resources' `inrun.dev/managed-since` with the current time, updating them. They now carry their owner's value, so an unchanged resource is left untouched.
-- Validation and mutation rules can target label and annotation keys with dots: escape them, as in `metadata.labels.app\.kubernetes\.io/name`. Such keys used to be split into nested fields.
-- Admission mutations patch keys containing `/` (such as `example.com/owner`) and objects without a `labels` or `annotations` map.
-- A serve target's own `onCreate`, `onReconcile` and `onDelete` templates were ignored when the CRD also had a `reconcile` block.
-- `inrun simulate` ignored the Simulate's `target`, so per-target hooks never ran.
-- `inrun simulate` dropped the status a reconciler returned.
-- `inrun e2e` repeated mutations such as `patch` and `scale` on every retry, and its teardown could hang on a resource whose controller was gone.
-
-## Earlier versions
-
-Inrun's history before the rename is Orkestra's: see the `archive` branch and
-the `orkestra-v0.7.18` tag.
+Orkestra's history is on the `archive` branch and the `orkestra-v0.7.18` tag.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 56ee5b5f6..c55685d98 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,78 +1,7 @@
# Contributing to Inrun
-Thank you for your interest in contributing to Inrun.
+Any help is welcome: a bug report, a docs fix, an example, code. The guide is
+[docs/contributing](docs/contributing/index.md): ways to help, setting up,
+and how pull requests work.
-Inrun is the intent runner for Kubernetes. A caller sends an intent through the gateway; Inrun turns it into a custom resource, reconciles it, and returns a view.
-
----
-
-## Start here
-
-Full contribution guides live in [`docs/contributing/`](docs/contributing/):
-
-| Guide | What it covers |
-|-------|---------------|
-| [index](docs/contributing/index.md) | Overview and where to go |
-| [Codebase map](docs/contributing/codebase-map.md) | Which packages belong to which binary; how to navigate the code |
-| [Resources](docs/contributing/contributing-resources.md) | Add or improve resource types in `pkg/resources` |
-| [Console](docs/contributing/contributing-console.md) | Improve the web UI — metrics, CR status, multi-instance views |
-| [Examples](docs/contributing/contributing-examples.md) | Add example operator packs |
-| [Publishing a new pack](docs/contributing/publishing-a-new-pack.md) | Exact checklist for adding a new examples pack without breaking CI |
-
----
-
-## Quick start
-
-```bash
-# Clone
-git clone https://github.com/intentd/inrun.git
-cd inrun
-
-# Build (runs codegen + gofmt + go build)
-make inrun
-
-# Test
-make test
-```
-
----
-
-## Pull requests
-
-1. Fork and create a branch from `main`.
-2. Read the relevant guide above.
-3. Run `make test-race` and confirm it passes (vet + unit tests + race detector).
-4. Open a PR with a short description of what and why. Link any related issues.
-
----
-
-## Commit messages
-
-Follow [Conventional Commits](https://www.conventionalcommits.org/):
-
-```
-feat(registry): add DaemonSet handler
-fix(reconciler): correct window-based rollback trigger
-docs(contributing): add resources contribution guide
-```
-
----
-
-## Code style
-
-- `gofmt` is enforced by CI.
-- Exported functions must have a doc comment.
-- Wrap errors with context: `fmt.Errorf("creating deployment: %w", err)`.
-- No comments that describe *what* the code does — only *why* when the reason is non-obvious.
-
----
-
-## Code of Conduct
-
-This project follows the [Inrun Code of Conduct](CODE_OF_CONDUCT.md). Report unacceptable behaviour to conduct@inrun.dev.
-
----
-
-## Questions?
-
-Open a [GitHub Discussion](https://github.com/intentd/inrun/discussions).
+Questions go to [GitHub Discussions](https://github.com/intentd/inrun/discussions).
diff --git a/Makefile b/Makefile
index 1dc527969..f06b1408d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build inrun-console clean test test-unit test-console test-race test-integration test-all test-coverage test-coverage-text vet vuln vuln-inrun vuln-console vuln-runtime vuln-gateway certs docs docs-sync docs-build docs-serve hugo-install generate-notes generate-e2e-example generate-resource-docs test-fixture-note test-fixture-reconciler inrun-gateway-linux docker-gateway gateway-reload runtime-reload console-reload reload docker-devserver release-devserver devserver-reload
+.PHONY: build inrun-console clean test test-unit test-console test-race test-integration test-all test-coverage test-coverage-text vet vuln vuln-inrun vuln-console vuln-runtime vuln-gateway certs docs docs-sync docs-build docs-serve hugo-install generate-functions generate-e2e-example generate-resource-docs test-fixture-reconciler inrun-gateway-linux docker-gateway gateway-reload runtime-reload console-reload reload docker-devserver release-devserver devserver-reload
# ── Configuration ────────────────────────────────────────────────────────────
INRUN_DIR := .
@@ -20,22 +20,22 @@ CONSOLE_LDFLAGS := -X github.com/intentd/inrun/console/version.Version=$(GIT_VE
-X github.com/intentd/inrun/console/version.Date=$(GIT_DATE)
# ── Local build ───────────────────────────────────────────────────────────
-generate-notes:
- @echo "Generating note list..."
- go run ./hack/generate-notes
- @echo "✅ Note list generated at pkg/note/list_generated.go"
+generate-functions:
+ @echo "Generating Inrun function registry and reference pages..."
+ go run ./hack/generate-functions
+ @echo "✅ Generated pkg/template/funcs/funcs_generated.go and docs/reference/inrun-functions"
generate-e2e-example:
@echo "Generating e2e complete example doc..."
@bash scripts/generate-e2e-example.sh
- @echo "✅ docs/reference/schema/04-e2e/08-complete-example.md updated"
+ @echo "✅ docs/reference/schema/e2e/complete-example.md updated"
generate-resource-docs:
@echo "Generating resource schema docs..."
go run ./hack/generate-resource-docs
- @echo "✅ docs/reference/schema/06-resources/*.md updated"
+ @echo "✅ docs/reference/schema/resources/*.md updated"
-inrun: generate-notes generate-e2e-example generate-resource-docs
+inrun: generate-functions generate-e2e-example generate-resource-docs
@echo "Building Inrun..."
@mkdir -p $(OUTPUT_DIR)
cd $(INRUN_DIR) && gofmt -w .
@@ -108,7 +108,7 @@ BIN := $(OUTPUT_DIR)/inrun
# ── Linux Build Targets (for Docker) ──────────────────────────────────────────
-inrun-linux: generate-notes
+inrun-linux: generate-functions
@echo "Building Inrun (Linux amd64)..."
@mkdir -p $(OUTPUT_DIR)
gofmt -w . && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \
@@ -123,7 +123,7 @@ inrun-console-linux:
cd $(CONSOLE_DIR) && gofmt -w . && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $(OUTPUT_DIR)/inrun-console .
@echo "✅ Linux Console binary built: $(OUTPUT_DIR)/inrun-console"
-inrun-gateway-linux: generate-notes
+inrun-gateway-linux: generate-functions
@echo "Building Inrun Gateway (Linux amd64)..."
@mkdir -p $(OUTPUT_DIR)
gofmt -w . && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \
@@ -136,7 +136,7 @@ inrun-gateway-linux: generate-notes
# These targets build Linux binaries to DOCKER_TMP (not OUTPUT_DIR) so they
# never overwrite the developer's local inrun CLI binary in ~/.inrun/bin.
-docker: generate-notes
+docker: generate-functions
@mkdir -p $(DOCKER_TMP)
@echo "Building Inrun runtime (Linux amd64) → $(DOCKER_TMP)/inrun..."
gofmt -w . && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \
@@ -161,7 +161,7 @@ docker-console:
@rm -f $(DOCKER_TMP)/inrun-console
@echo "✔ Docker image built: $(INRUN_CONSOLE_IMAGE)"
-docker-gateway: generate-notes
+docker-gateway: generate-functions
@mkdir -p $(DOCKER_TMP)
@echo "Building Inrun gateway (Linux amd64) → $(DOCKER_TMP)/inrun-gateway..."
gofmt -w . && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \
@@ -365,34 +365,11 @@ test-coverage-text:
go test ./pkg/... -coverprofile=coverage.out -covermode=atomic -count=1
@go tool cover -func=coverage.out | tail -5
-# ── Fixture tests (kind cluster required) ────────────────────────────────────
-# Each target spins up a dedicated kind cluster, installs Inrun via Helm,
-# applies the fixture catalog + CR, asserts resources exist, then tears down.
-# Requires: kind, helm, kubectl, inrun — all on $PATH.
-
-FIXTURE_NOTE_CLUSTER := inrun-note-fixture
-FIXTURE_RECONCILER_CLUSTER := inrun-reconciler-fixture
-HELM_CHART := ./charts/inrun
-
-test-fixture-note:
- @echo "── Note fixture test ──────────────────────────────────────────"
- @bash scripts/setup-kind.sh $(FIXTURE_NOTE_CLUSTER)
- @kubectl apply -f pkg/note/fixture/crd.yaml
- @inrun generate bundle --file pkg/note/fixture/catalog.yaml | kubectl apply -f -
- @helm install inrun $(HELM_CHART) --namespace inrun-system --wait --timeout 120s
- @kubectl apply -f pkg/note/fixture/cr.yaml
- @kubectl wait reconcilerprobe/my-probe --for=jsonpath='{.status.phase}'=Ready \
- --timeout=120s 2>/dev/null || kubectl wait noteprobe/my-probe \
- --for=condition=Ready=true --timeout=120s || \
- kubectl get noteprobe my-probe -o yaml
- @echo "✅ Note fixture passed"
- @bash scripts/setup-kind.sh delete $(FIXTURE_NOTE_CLUSTER)
-
test-fixture-reconciler:
@echo "── Reconciler fixture test ────────────────────────────────────"
@bash scripts/setup-kind.sh $(FIXTURE_RECONCILER_CLUSTER)
@kubectl apply -f pkg/runtime/reconciler/fixture/crd.yaml
- @inrun generate bundle --file pkg/runtime/reconciler/fixture/catalog.yaml | kubectl apply -f -
+ @inrun generate bundle --file pkg/runtime/reconciler/fixture/blueprint.yaml | kubectl apply -f -
@helm install inrun $(HELM_CHART) --namespace inrun-system --wait --timeout 120s
@kubectl apply -f pkg/runtime/reconciler/fixture/cr.yaml
@kubectl wait reconcilerprobe/probe --for=jsonpath='{.status.tier}'=premium \
@@ -469,7 +446,7 @@ vet:
# (pkg/registry/client.go, pkg/registry/module/pull.go) are both build-tag
# excluded from the runtime and gateway binaries — see the comments atop
# those files. Both are authoring-time-only (inrun push/pull/generate bundle);
-# the runtime and gateway only ever read an already-merged catalog.yaml key
+# the runtime and gateway only ever read an already-merged blueprint.yaml key
# from a ConfigMap. validate-pr.yml's vuln-check (console/runtime/
# gateway) has no known findings left to accept — it runs as a hard gate.
#
@@ -480,7 +457,7 @@ vet:
# NewPullWithOpts starts from a zero-value Pull{} (Verify defaults to
# false), and Inrun's own call site never sets pull.Verify = true —
# so the verification code path that would invoke openpgp is never
-# exercised, regardless of what a Catalog's HelmSource points at.
+# exercised, regardless of what a Blueprint's HelmSource points at.
# - GO-2026-5622 / GO-2026-5338 / GO-2026-5064 (github.com/containerd/containerd,
# no fix yet): all three are specifically about CRI checkpoint/restore, a
# container-runtime feature nothing in this codebase calls. containerd is
diff --git a/README.md b/README.md
index 30ec74652..ded8ada1d 100644
--- a/README.md
+++ b/README.md
@@ -4,63 +4,113 @@
Run intents on Kubernetes.
+
-
-
+
+
-A caller sends an **intent**: a small JSON payload in their own words, with no
-`apiVersion`, `kind` or `spec`. Inrun turns it into a custom resource,
-reconciles it, and gives the caller back a **view** of the result.
+Kubernetes reconciliation is battle-tested and free. Using it is expensive: a purpose-built operator, Kubernetes expertise, and maintenance for as long as it runs. So developers leave it to platform engineers.
+
+Inrun opens it to anyone who has written a service that takes a request and returns a response.
+
+You bring the decision. Inrun brings the machinery.
+
+## How it works
+
+A caller sends an **intent**: a small payload in their own words, with no `apiVersion`, `kind` or `spec`. Inrun turns it into a custom resource, reconciles it, and gives the caller back a **view** of the result.
```console
intent ──▶ gateway ──▶ custom resource ──▶ runtime ──▶ reconcile ──▶ status ──▶ view
```
-How a resource is reconciled is up to you:
+**The reconciler decides. Inrun writes.** Inrun runs the informers, queues, retries, leader election, server-side apply, owner references, finalizers, status, events, cleanup and metrics. How a resource is reconciled is up to you:
-- **Declarative:** describe the resources in a Catalog; no code.
-- **Remote:** any HTTP endpoint, in any language, returns what should exist.
+- **Declarative:** describe the resources in a Blueprint. No code.
+- **Remote:** any HTTP service, in any language, returns what should exist.
+- **Existing service:** an API that knows nothing about Inrun reconciles as it is. Inrun calls it once per reconcile and maps its answer into status.
- **Typed:** Go hooks, a full reconciler, or an unchanged controller-runtime `Reconcile`.
-Inrun runs the informers, queues, workers, retries, leader election, status,
-cleanup and metrics.
+Declarative, remote and typed reconcilers take the same request and return the same result: status, resources, labels and annotations, a requeue, or a cleanup.
-> **Status:** pre-release. The first release, `v0.1.0`, is in preparation, and
-> the API may still change before `v1.0`.
+## What comes with it
+
+- **Operators as data.** A Blueprint is the operator. It's versioned and published to an OCI registry, carrying its own simulate and e2e results and the tests that produced them. `inrun inspect` shows them before you install anything; `inrun e2e` runs them again on a kind cluster it brings and tears down, so checking the claim needs nothing but Docker. See [examples/registry](examples/registry/).
+- **Callers apart from the schema.** Callers send intents; the CRD stays an internal detail. When a field moves, `migrate.from` brings stored resources to the new shape with the same mapping the gateway applies; when the CRD itself changes (a new kind, group or scope), `migrate.to` moves them to it. Either way, callers never notice. See [examples/migration](examples/migration/).
+- **Kubernetes as a backend.** A reconciler doesn't have to create anything: the custom resource can be a durable state machine, cleaned up when it's done. See [examples/reconcilers/remote/existing-service-ids](examples/reconcilers/remote/existing-service-ids/).
+
+## Install
+
+```bash
+brew install intentd/tap/inrun
+# or
+curl -sSL https://get.inrun.dev | bash
+```
## Try it
-Build the CLI from source (Go 1.26+):
+Two commands to your first operator:
```bash
-go build -o inrun ./cmd/inrun
+inrun init # a hello-website operator in the current directory
+inrun # run it on your current cluster; the console opens on :8081
```
-Then run the first example against a cluster in your current kubeconfig:
+No cluster? Add `--dev` to create a temporary kind cluster (needs Docker). Or check it without one, in memory:
```bash
-cd examples/reconcilers/declarative/01-hello-website
+inrun simulate
```
+Or start from a published pattern:
+
```bash
-inrun
+inrun inspect postgres:v0.1.0 # version, description, test results
+inrun postgres:v0.1.0 --apply-cr --dev # run it - all batteries included
```
+`postgres:v0.1.0` is short for `ghcr.io/intentd/registry/patterns/blueprints/postgres:v0.1.0`. Patterns can live in any OCI registry: set `INRUN_REGISTRY` to use your own. The published ones are built from [inrun-registry](https://github.com/intentd/inrun-registry).
+
## Examples
-| Pack | Shows |
-| --- | --- |
-| [declarative](examples/reconcilers/declarative/) | Operators declared in a Catalog. Start here. |
-| [remote](examples/reconcilers/remote/) | A reconciler in any language, called over HTTP |
-| [typed](examples/reconcilers/typed/) | Go hooks, a constructor, and a controller-runtime drop-in |
-| [intent](examples/intent/) | Intent in through the gateway, view out |
+Examples come in packs. Copy one into a new project with `inrun init --pack`:
+
+```bash
+inrun init --pack declarative # Start here: operators declared in a Blueprint, no code
+inrun init --pack remote # A reconciler in any language, called over HTTP, including an existing service
+inrun init --pack typed # Go: hooks, a constructor, and a controller-runtime drop-in
+inrun init --pack intent # Intent in through the gateway, view out
+inrun init --pack migration # Change the CRD under live CRs: move fields, or move CRs to another CRD
+inrun init --pack registry # Share operators as patterns: consume one, publish your own, deprecate it
+```
+
+`inrun init --list` shows them all. Each pack is a folder of examples; for any of them:
+
+```bash
+cd declarative/01-hello-website
+inrun # run it
+inrun simulate # check it without a cluster
+inrun simulate --envtest # the same against a real API server and etcd, still no cluster
+inrun e2e # test it end to end on a kind cluster it creates and removes
+```
+
+Each example's README is its guide.
+
+## Built on
-Each example can be checked without a cluster (`inrun simulate`) and end to
-end on kind (`inrun e2e`).
+Two libraries that came out of Inrun and stand on their own:
+
+- [KEL](https://github.com/intentd/kel): functions that know Kubernetes, for reading state in templates and Go.
+- [kubewright](https://github.com/intentd/kubewright): building and writing an owner's child resources.
+
+## Status
+
+`v0.1.0` is the first release. The API may still change before `v1.0`; breaking changes are listed in [CHANGELOG.md](CHANGELOG.md).
+
+Inrun began as Orkestra. Its history is on the `archive` branch and the `orkestra-v0.7.18` tag.
## Development
@@ -68,13 +118,7 @@ end on kind (`inrun e2e`).
make test
```
-See [tests/README.md](tests/README.md) for the test tiers and
-[docs/contributing](docs/contributing/) for how the code is laid out.
-
-## History
-
-Inrun began as Orkestra. Its history is on the `archive` branch and the
-`orkestra-v0.7.18` tag.
+See [tests/README.md](tests/README.md) for the test tiers and [docs/contributing](docs/contributing/) for how the code is laid out.
## License
diff --git a/SECURITY.md b/SECURITY.md
index 56c099d98..0758040dd 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -43,6 +43,6 @@ We will acknowledge your report within **72 hours**.
## What Is *Not* Considered a Security Issue?
- Misconfigured RBAC in user clusters
-- Incorrect Catalog definitions
+- Incorrect Blueprint definitions
- Expected Kubernetes behavior
- Resource exhaustion caused by user‑defined CRDs
diff --git a/charts/examples/platform-catalog/Chart.yaml b/charts/examples/platform-blueprint/Chart.yaml
similarity index 77%
rename from charts/examples/platform-catalog/Chart.yaml
rename to charts/examples/platform-blueprint/Chart.yaml
index 6a68c78c4..bdaec9a73 100644
--- a/charts/examples/platform-catalog/Chart.yaml
+++ b/charts/examples/platform-blueprint/Chart.yaml
@@ -1,7 +1,7 @@
apiVersion: v1
-name: platform-catalog
+name: platform-blueprint
description: >-
- Example Helm chart that renders an Inrun Catalog.
+ Example Helm chart that renders an Inrun Blueprint.
Demonstrates how to ship CRD definitions as a Helm chart
so teams can consume them via sources.helm in their Stack.
type: application
@@ -10,7 +10,7 @@ appVersion: "1.0.0"
keywords:
- inrun
- stack
- - catalog
+ - blueprint
- operator
home: https://github.com/intentd/inrun
maintainers:
diff --git a/charts/examples/platform-blueprint/README.md b/charts/examples/platform-blueprint/README.md
new file mode 100644
index 000000000..0742ca808
--- /dev/null
+++ b/charts/examples/platform-blueprint/README.md
@@ -0,0 +1,23 @@
+# platform-blueprint
+
+A Helm chart that renders an Inrun Blueprint and nothing else: no CRDs, no
+operator. A Stack pulls it in through `sources.helm`.
+
+## Values
+
+| Field | Default | Description |
+| --- | --- | --- |
+| `apiGroup` | `demo.inrun.dev` | API group for every CRD |
+| `apiVersion` | `v1alpha1` | API version for every CRD |
+| `crds` | `database`, `cache` | Map of CRD name to `kind`, `plural`, `namespaced`, `enabled`, `reconcile.workers`, `reconcile.resync`, `description` |
+
+## Use it from a Stack
+
+```yaml
+sources:
+ helm:
+ - repo: https://github.com/intentd/inrun.git
+ path: charts/examples/platform-blueprint
+ chart: platform-blueprint
+ version: main
+```
diff --git a/charts/examples/platform-catalog/cr.yaml b/charts/examples/platform-blueprint/cr.yaml
similarity index 100%
rename from charts/examples/platform-catalog/cr.yaml
rename to charts/examples/platform-blueprint/cr.yaml
diff --git a/charts/examples/platform-catalog/crd.yaml b/charts/examples/platform-blueprint/crd.yaml
similarity index 100%
rename from charts/examples/platform-catalog/crd.yaml
rename to charts/examples/platform-blueprint/crd.yaml
diff --git a/charts/examples/platform-blueprint/templates/blueprint.yaml b/charts/examples/platform-blueprint/templates/blueprint.yaml
new file mode 100644
index 000000000..2eb0b4742
--- /dev/null
+++ b/charts/examples/platform-blueprint/templates/blueprint.yaml
@@ -0,0 +1,33 @@
+apiVersion: inrun.dev/v1
+kind: Blueprint
+metadata:
+ name: {{ .Release.Name }}-blueprint
+ version: 0.1.0
+ author: intentd
+ description: >
+ blueprint.yaml Helm template — renders an Inrun Blueprint from Helm values.
+ Inrun's merger reads this template's output and extracts CRD definitions.
+ The chart must render at least one template with kind Blueprint.
+ tags:
+ - helm
+ - composition
+ - data
+
+spec:
+ crds:
+ {{- range $name, $crd := .Values.crds }}
+ {{ $name }}:
+ enabled: {{ if hasKey $crd "enabled" }}{{ $crd.enabled }}{{ else }}true{{ end }}
+ namespaced: {{ if hasKey $crd "namespaced" }}{{ $crd.namespaced }}{{ else }}true{{ end }}
+ operatorBox:
+ reconcile:
+ workers: {{ $crd.reconcile.workers | default 2 }}
+ resync: {{ $crd.reconcile.resync | default "30s" }}
+
+ apiTypes:
+ group: {{ $.Values.apiGroup }}
+ version: {{ $.Values.apiVersion | default "v1alpha1" }}
+ kind: {{ $crd.kind }}
+ plural: {{ $crd.plural }}
+ description: {{ $crd.description | default "" | quote }}
+ {{- end }}
\ No newline at end of file
diff --git a/charts/examples/platform-blueprint/values.yaml b/charts/examples/platform-blueprint/values.yaml
new file mode 100644
index 000000000..313d949c2
--- /dev/null
+++ b/charts/examples/platform-blueprint/values.yaml
@@ -0,0 +1,33 @@
+# Default values for platform-blueprint.
+# Override these in your own values file or via --set.
+
+# apiGroup — the Kubernetes API group for all CRDs in this chart.
+apiGroup: demo.inrun.dev
+
+# apiVersion — the API version for all CRDs in this chart.
+apiVersion: v1alpha1
+
+# crds — map of CRD definitions to include in the rendered Blueprint.
+# Each key is the CRD name and produces one CRDEntry in the output.
+crds:
+ database:
+ enabled: true
+ kind: Database
+ plural: databases
+ namespaced: false
+ reconcile:
+ workers: 3
+ resync: 1m
+ description: >-
+ Manages a database instance. Provisioned by the platform team Helm chart.
+
+ cache:
+ enabled: true
+ kind: Cache
+ plural: caches
+ namespaced: true
+ reconcile:
+ workers: 3
+ resync: 2m
+ description: >-
+ Manages a cache instance. Provisioned by the platform team Helm chart.
\ No newline at end of file
diff --git a/charts/examples/platform-catalog/README.md b/charts/examples/platform-catalog/README.md
deleted file mode 100644
index d785cacc0..000000000
--- a/charts/examples/platform-catalog/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# platform-catalog
-
-A Helm chart that renders an Inrun Catalog and nothing else: no CRDs, no
-operator. A Stack pulls it in through `sources.helm`.
-
-## Values
-
-| Field | Default | Description |
-| --- | --- | --- |
-| `apiGroup` | `demo.inrun.dev` | API group for every CRD |
-| `apiVersion` | `v1alpha1` | API version for every CRD |
-| `crds` | `database`, `cache` | Map of CRD name to `kind`, `plural`, `namespaced`, `enabled`, `reconcile.workers`, `reconcile.resync`, `description` |
-
-## Use it from a Stack
-
-```yaml
-sources:
- helm:
- - repo: https://github.com/intentd/inrun.git
- path: charts/examples/platform-catalog
- chart: platform-catalog
- version: main
-```
diff --git a/charts/examples/platform-catalog/templates/catalog.yaml b/charts/examples/platform-catalog/templates/catalog.yaml
deleted file mode 100644
index f7cb31c2a..000000000
--- a/charts/examples/platform-catalog/templates/catalog.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-apiVersion: inrun.dev/v1
-kind: Catalog
-metadata:
- name: {{ .Release.Name }}-catalog
- version: 0.1.0
- author: intentd
- description: >
- catalog.yaml Helm template — renders an Inrun Catalog from Helm values.
- Inrun's merger reads this template's output and extracts CRD definitions.
- The chart must render at least one template with kind Catalog.
- tags:
- - helm
- - composition
- - data
-
-spec:
- crds:
- {{- range $name, $crd := .Values.crds }}
- {{ $name }}:
- enabled: {{ if hasKey $crd "enabled" }}{{ $crd.enabled }}{{ else }}true{{ end }}
- namespaced: {{ if hasKey $crd "namespaced" }}{{ $crd.namespaced }}{{ else }}true{{ end }}
- operatorBox:
- reconcile:
- workers: {{ $crd.reconcile.workers | default 2 }}
- resync: {{ $crd.reconcile.resync | default "30s" }}
-
- apiTypes:
- group: {{ $.Values.apiGroup }}
- version: {{ $.Values.apiVersion | default "v1alpha1" }}
- kind: {{ $crd.kind }}
- plural: {{ $crd.plural }}
- description: {{ $crd.description | default "" | quote }}
- {{- end }}
\ No newline at end of file
diff --git a/charts/examples/platform-catalog/values.yaml b/charts/examples/platform-catalog/values.yaml
deleted file mode 100644
index ca91974bf..000000000
--- a/charts/examples/platform-catalog/values.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-# Default values for platform-catalog.
-# Override these in your own values file or via --set.
-
-# apiGroup — the Kubernetes API group for all CRDs in this chart.
-apiGroup: demo.inrun.dev
-
-# apiVersion — the API version for all CRDs in this chart.
-apiVersion: v1alpha1
-
-# crds — map of CRD definitions to include in the rendered Catalog.
-# Each key is the CRD name and produces one CRDEntry in the output.
-crds:
- database:
- enabled: true
- kind: Database
- plural: databases
- namespaced: false
- reconcile:
- workers: 3
- resync: 1m
- description: >-
- Manages a database instance. Provisioned by the platform team Helm chart.
-
- cache:
- enabled: true
- kind: Cache
- plural: caches
- namespaced: true
- reconcile:
- workers: 3
- resync: 2m
- description: >-
- Manages a cache instance. Provisioned by the platform team Helm chart.
\ No newline at end of file
diff --git a/charts/inrun/README.md b/charts/inrun/README.md
index 981b31940..ac0c85c5e 100644
--- a/charts/inrun/README.md
+++ b/charts/inrun/README.md
@@ -6,7 +6,7 @@ view.
This chart deploys:
-- **Runtime:** reconciles the custom resources declared in your Catalog.
+- **Runtime:** reconciles the custom resources declared in your Blueprint.
- **Gateway:** takes intents in, serves admission webhooks, and enforces
deletion and namespace protection.
- **Console:** a web UI over one or more runtimes.
@@ -23,8 +23,8 @@ curl -sSL https://get.inrun.dev | bash
## Install
-Inrun does not create RBAC or its Catalog ConfigMap itself. You generate them
-from your Catalog, review them, and apply them, so every permission is visible
+Inrun does not create RBAC or its Blueprint ConfigMap itself. You generate them
+from your Blueprint, review them, and apply them, so every permission is visible
before it reaches the cluster.
### 1. Install the CRD
@@ -35,13 +35,13 @@ This uses the Website CRD from the hello-website example:
kubectl apply -f https://raw.githubusercontent.com/intentd/inrun/main/examples/reconcilers/declarative/01-hello-website/manifests/crd.yaml
```
-### 2. Write a Catalog
+### 2. Write a Blueprint
-Save as `catalog.yaml`:
+Save as `blueprint.yaml`:
```yaml
apiVersion: inrun.dev/v1
-kind: Catalog
+kind: Blueprint
metadata:
name: hello-website
spec:
@@ -71,16 +71,16 @@ spec:
### 3. Review the permissions
```bash
-inrun validate --full -f catalog.yaml
+inrun validate --full -f blueprint.yaml
```
-This prints every RBAC rule the Catalog needs, per CRD and per component, with
+This prints every RBAC rule the Blueprint needs, per CRD and per component, with
a note on why each one exists. No cluster required.
### 4. Generate and apply the bundle
```bash
-inrun generate bundle -f catalog.yaml -o bundle.yaml
+inrun generate bundle -f blueprint.yaml -o bundle.yaml
```
```bash
@@ -88,8 +88,8 @@ kubectl apply -f bundle.yaml
```
The bundle holds the `inrun-system` Namespace, the ServiceAccounts, a
-ClusterRole with only the permissions the Catalog needs, its binding, and the
-`inrun-catalog` ConfigMap. Commit it to your GitOps repo if you use one.
+ClusterRole with only the permissions the Blueprint needs, its binding, and the
+`inrun-blueprint` ConfigMap. Commit it to your GitOps repo if you use one.
### 5. Install the chart
@@ -121,7 +121,7 @@ The runtime and gateway deploy independently.
| Gateway only | `false` | `true` | Deletion and namespace protection only |
| Full | `true` | `true` | Intents in, reconcile, views out |
-For gateway only, set `gateway.standalone: true` in the Catalog and generate
+For gateway only, set `gateway.standalone: true` in the Blueprint and generate
with `inrun generate bundle --for gateway`.
## Values
@@ -142,8 +142,8 @@ with `inrun generate bundle --for gateway`.
| `runtime.config.maxDepth` | Max queue depth per CRD | `500` |
| `runtime.config.failureThreshold` | Failures before degraded | `10` |
| `runtime.leaderElection.enabled` | Leader election | `true` |
-| `runtime.catalog.existingConfigMap` | Catalog ConfigMap | `-catalog` |
-| `runtime.catalog.configMapKey` | Key in the ConfigMap | `catalog.yaml` |
+| `runtime.blueprint.existingConfigMap` | Blueprint ConfigMap | `-blueprint` |
+| `runtime.blueprint.configMapKey` | Key in the ConfigMap | `blueprint.yaml` |
| `runtime.gatewayEndpoint` | Gateway URL shown to the console | `""` |
| `runtime.registry.url` | Pattern registry | `""` |
@@ -159,7 +159,7 @@ with `inrun generate bundle --for gateway`.
| `gateway.server.httpsPort` | Webhook port | `8443` |
| `gateway.webhooks.enabled` | Serve webhooks with your own TLS secret | `false` |
| `gateway.webhooks.existingSecret` | TLS secret for webhooks | `""` |
-| `gateway.catalog.existingConfigMap` | Catalog ConfigMap | the runtime's |
+| `gateway.blueprint.existingConfigMap` | Blueprint ConfigMap | the runtime's |
| `gateway.ingress.enabled` | Expose the Gateway API | `false` |
### Console
diff --git a/charts/inrun/e2e.yaml b/charts/inrun/e2e.yaml
index edce6c5a1..b33bcb8ba 100644
--- a/charts/inrun/e2e.yaml
+++ b/charts/inrun/e2e.yaml
@@ -29,7 +29,7 @@ spec:
- path: fixtures/inrun-bundle.yaml
wait:
- kind: ConfigMap
- name: inrun-catalog
+ name: inrun-blueprint
namespace: inrun-system
timeout: 30s
- path: fixtures/cr.yaml
@@ -120,7 +120,7 @@ spec:
leaderElection:
lease: inrun-leader
port: 8080
- path: /catalog/website/health
+ path: /blueprint/website/health
jq: resourceCount
equals: "1"
@@ -128,7 +128,7 @@ spec:
leaderElection:
lease: inrun-leader
port: 8080
- path: /catalog/website/health
+ path: /blueprint/website/health
jq: healthy
equals: "true"
@@ -136,8 +136,8 @@ spec:
leaderElection:
lease: inrun-leader
port: 8080
- path: /catalog/website
- outputContains: "Website CRD, GVK: demo.inrun.dev/v1alpha1, Kind=Website"
+ path: /blueprint/website
+ contains: "Website CRD, GVK: demo.inrun.dev/v1alpha1, Kind=Website"
- include: ./e2e/cleanup.yaml
diff --git a/charts/inrun/fixtures/blueprint.yaml b/charts/inrun/fixtures/blueprint.yaml
new file mode 100644
index 000000000..4c1ae0007
--- /dev/null
+++ b/charts/inrun/fixtures/blueprint.yaml
@@ -0,0 +1,45 @@
+apiVersion: inrun.dev/v1
+kind: Blueprint
+metadata:
+ name: hello-website
+ version: 0.1.0
+ description: Fixture blueprint for chart e2e — one CRD, one Deployment, one Service.
+
+security:
+ namespaceProtection:
+ enabled: true
+ cleanupOnShutdown: true
+
+gateway:
+ enabled: true
+
+spec:
+ crds:
+ website:
+ apiTypes:
+ group: demo.inrun.dev
+ version: v1alpha1
+ kind: Website
+ plural: websites
+ operatorBox:
+ reconcile:
+ onCreate:
+ deployments:
+ - name: "{{ .metadata.name }}"
+ image: "{{ .spec.image }}"
+ replicas: "{{ .spec.replicas }}"
+ port: "{{ .spec.port }}"
+ reconcile: true
+ services:
+ - name: "{{ .metadata.name }}-svc"
+ port: "80"
+ targetPort: "{{ .spec.port }}"
+ reconcile: true
+ emit:
+ status:
+ fields:
+ - path: phase
+ value: "Running"
+ runtime:
+ allowedNamespaces:
+ - default
diff --git a/charts/inrun/fixtures/catalog.yaml b/charts/inrun/fixtures/catalog.yaml
deleted file mode 100644
index 8e95ef607..000000000
--- a/charts/inrun/fixtures/catalog.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-apiVersion: inrun.dev/v1
-kind: Catalog
-metadata:
- name: hello-website
- version: 0.1.0
- description: Fixture catalog for chart e2e — one CRD, one Deployment, one Service.
-
-security:
- namespaceProtection:
- enabled: true
- cleanupOnShutdown: true
-
-gateway:
- enabled: true
-
-spec:
- crds:
- website:
- apiTypes:
- group: demo.inrun.dev
- version: v1alpha1
- kind: Website
- plural: websites
- operatorBox:
- reconcile:
- onCreate:
- deployments:
- - name: "{{ .metadata.name }}"
- image: "{{ .spec.image }}"
- replicas: "{{ .spec.replicas }}"
- port: "{{ .spec.port }}"
- reconcile: true
- services:
- - name: "{{ .metadata.name }}-svc"
- port: "80"
- targetPort: "{{ .spec.port }}"
- reconcile: true
- emit:
- status:
- fields:
- - path: phase
- value: "Running"
- runtime:
- allowedNamespaces:
- - default
diff --git a/charts/inrun/fixtures/inrun-bundle.yaml b/charts/inrun/fixtures/inrun-bundle.yaml
index 512427604..6b4176a58 100644
--- a/charts/inrun/fixtures/inrun-bundle.yaml
+++ b/charts/inrun/fixtures/inrun-bundle.yaml
@@ -189,20 +189,20 @@ subjects:
---
apiVersion: v1
data:
- catalog.yaml: |
+ blueprint.yaml: |
apiVersion: inrun.dev/v1
- kind: Catalog
+ kind: Blueprint
metadata:
name: hello-website
- description: Fixture catalog for chart e2e — one CRD, one Deployment, one Service.
+ description: Fixture blueprint for chart e2e — one CRD, one Deployment, one Service.
version: 0.1.0
spec:
crds:
website:
- catalogName: hello-website
- catalogNamespace: default
- catalogDescription: Fixture catalog for chart e2e — one CRD, one Deployment, one Service.
- catalogVersion: 0.1.0
+ blueprintName: hello-website
+ blueprintNamespace: default
+ blueprintDescription: Fixture blueprint for chart e2e — one CRD, one Deployment, one Service.
+ blueprintVersion: 0.1.0
description: 'Website CRD, GVK: demo.inrun.dev/v1alpha1, Kind=Website'
mode: dynamic
apiTypes:
@@ -251,6 +251,6 @@ metadata:
app.kubernetes.io/name: inrun
app.kubernetes.io/tag: inrun-internal
inrun.dev/deletion-protection: "true"
- name: inrun-catalog
+ name: inrun-blueprint
namespace: inrun-system
diff --git a/charts/inrun/templates/_helpers.tpl b/charts/inrun/templates/_helpers.tpl
index 771674404..4afbb7ced 100644
--- a/charts/inrun/templates/_helpers.tpl
+++ b/charts/inrun/templates/_helpers.tpl
@@ -65,13 +65,13 @@ Runtime image — respects tag override, falls back to appVersion.
{{- end }}
{{/*
-Catalog ConfigMap the runtime mounts.
+Blueprint ConfigMap the runtime mounts.
*/}}
-{{- define "inrun.catalogConfigMapName" -}}
-{{- if .Values.runtime.catalog.existingConfigMap }}
-{{- .Values.runtime.catalog.existingConfigMap }}
+{{- define "inrun.blueprintConfigMapName" -}}
+{{- if .Values.runtime.blueprint.existingConfigMap }}
+{{- .Values.runtime.blueprint.existingConfigMap }}
{{- else }}
-{{- printf "%s-catalog" (include "inrun.fullname" .) }}
+{{- printf "%s-blueprint" (include "inrun.fullname" .) }}
{{- end }}
{{- end }}
@@ -87,10 +87,10 @@ Gateway image — respects tag override, falls back to appVersion.
{{- end }}
{{/*
-Catalog ConfigMap the gateway mounts — its own, or the runtime's.
+Blueprint ConfigMap the gateway mounts — its own, or the runtime's.
*/}}
-{{- define "inrun.gatewayCatalogConfigMapName" -}}
-{{- default (include "inrun.catalogConfigMapName" .) .Values.gateway.catalog.existingConfigMap }}
+{{- define "inrun.gatewayBlueprintConfigMapName" -}}
+{{- default (include "inrun.blueprintConfigMapName" .) .Values.gateway.blueprint.existingConfigMap }}
{{- end }}
{{/*
diff --git a/charts/inrun/templates/gateway-deployment.yaml b/charts/inrun/templates/gateway-deployment.yaml
index ebfb46074..1cce7ded6 100644
--- a/charts/inrun/templates/gateway-deployment.yaml
+++ b/charts/inrun/templates/gateway-deployment.yaml
@@ -52,7 +52,7 @@ spec:
args:
- --file
- - {{ .Values.gateway.catalog.mountPath }}/{{ .Values.gateway.catalog.configMapKey }}
+ - {{ .Values.gateway.blueprint.mountPath }}/{{ .Values.gateway.blueprint.configMapKey }}
env:
- name: LOG_LEVEL
@@ -138,8 +138,8 @@ spec:
mountPath: /tls
readOnly: true
{{- end }}
- - name: catalog
- mountPath: {{ .Values.gateway.catalog.mountPath }}
+ - name: blueprint
+ mountPath: {{ .Values.gateway.blueprint.mountPath }}
readOnly: true
- name: tmp
mountPath: /tmp
@@ -153,9 +153,9 @@ spec:
secret:
secretName: {{ $webhooks.existingSecret }}
{{- end }}
- - name: catalog
+ - name: blueprint
configMap:
- name: {{ include "inrun.gatewayCatalogConfigMapName" . }}
+ name: {{ include "inrun.gatewayBlueprintConfigMapName" . }}
- name: tmp
emptyDir: {}
{{- with .Values.gateway.extraVolumes }}
diff --git a/charts/inrun/templates/networkpolicy.yaml b/charts/inrun/templates/networkpolicy.yaml
index c931eb152..58fde74a0 100644
--- a/charts/inrun/templates/networkpolicy.yaml
+++ b/charts/inrun/templates/networkpolicy.yaml
@@ -26,7 +26,7 @@ spec:
{{- end }}
egress:
# Allow all egress — Inrun must reach the Kubernetes API server
- # and any remote Catalog sources (URLs, Helm repos)
+ # and any remote Blueprint sources (URLs, Helm repos)
- {}
{{- end }}
---
diff --git a/charts/inrun/templates/runtime-deployment.yaml b/charts/inrun/templates/runtime-deployment.yaml
index c53dff4b8..42c11ed98 100644
--- a/charts/inrun/templates/runtime-deployment.yaml
+++ b/charts/inrun/templates/runtime-deployment.yaml
@@ -45,7 +45,7 @@ spec:
args:
- --file
- - {{ .Values.runtime.catalog.mountPath }}/{{ .Values.runtime.catalog.configMapKey }}
+ - {{ .Values.runtime.blueprint.mountPath }}/{{ .Values.runtime.blueprint.configMapKey }}
env:
- name: LOG_LEVEL
@@ -131,8 +131,8 @@ spec:
{{- toYaml .Values.runtime.resources | nindent 12 }}
volumeMounts:
- - name: catalog
- mountPath: {{ .Values.runtime.catalog.mountPath }}
+ - name: blueprint
+ mountPath: {{ .Values.runtime.blueprint.mountPath }}
readOnly: true
- name: tmp
mountPath: /tmp
@@ -141,9 +141,9 @@ spec:
{{- end }}
volumes:
- - name: catalog
+ - name: blueprint
configMap:
- name: {{ include "inrun.catalogConfigMapName" . }}
+ name: {{ include "inrun.blueprintConfigMapName" . }}
- name: tmp
emptyDir: {}
{{- with .Values.runtime.extraVolumes }}
diff --git a/charts/inrun/values.schema.json b/charts/inrun/values.schema.json
index eeb5ffe06..9a5a7dc52 100644
--- a/charts/inrun/values.schema.json
+++ b/charts/inrun/values.schema.json
@@ -65,7 +65,7 @@
"retryPeriod": { "type": "integer" }
}
},
- "catalog": {
+ "blueprint": {
"type": "object",
"properties": {
"existingConfigMap": { "type": "string" },
@@ -158,7 +158,7 @@
"keyKey": { "type": "string" }
}
},
- "catalog": {
+ "blueprint": {
"type": "object",
"properties": {
"existingConfigMap": { "type": "string" },
diff --git a/charts/inrun/values.yaml b/charts/inrun/values.yaml
index c43729321..6cb1ac50d 100644
--- a/charts/inrun/values.yaml
+++ b/charts/inrun/values.yaml
@@ -89,13 +89,13 @@ runtime:
renewDeadline: 10
retryPeriod: 5
- # Catalog (runtime only)
- catalog:
- existingConfigMap: "" # ConfigMap from 'inrun generate bundle'; default -catalog
- configMapKey: catalog.yaml
- mountPath: /etc/inrun/catalog
+ # Blueprint (runtime only)
+ blueprint:
+ existingConfigMap: "" # ConfigMap from 'inrun generate bundle'; default -blueprint
+ configMapKey: blueprint.yaml
+ mountPath: /etc/inrun/blueprint
- # Gateway endpoint advertised in the runtime /catalog response.
+ # Gateway endpoint advertised in the runtime /blueprint response.
# Set to the HTTP base URL of the companion gateway deployment so the
# console can discover and merge gateway stats.
# Overrides INRUN_GATEWAY_ENDPOINT env. Empty = no gateway configured.
@@ -259,11 +259,11 @@ gateway:
certKey: tls.crt # key in secret for certificate
keyKey: tls.key # key in secret for private key
- # Catalog (needed to know which CRDs have webhooks)
- catalog:
- existingConfigMap: "" # default: runtime.catalog's ConfigMap
- configMapKey: catalog.yaml
- mountPath: /etc/inrun/catalog
+ # Blueprint (needed to know which CRDs have webhooks)
+ blueprint:
+ existingConfigMap: "" # default: runtime.blueprint's ConfigMap
+ configMapKey: blueprint.yaml
+ mountPath: /etc/inrun/blueprint
# Registry
registry:
diff --git a/cmd/cli/cluster/clusters.go b/cmd/cli/cluster/clusters.go
index 38f6f53b5..22311d9e5 100644
--- a/cmd/cli/cluster/clusters.go
+++ b/cmd/cli/cluster/clusters.go
@@ -9,8 +9,8 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/validate"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/validate"
apigateway "github.com/intentd/inrun/pkg/gateway/api"
"github.com/intentd/inrun/pkg/tools/cluster"
"github.com/intentd/inrun/pkg/types"
@@ -25,26 +25,26 @@ var clustersCmd = &cobra.Command{
Long: `List all clusters registered in gateway.clusters.
Shows the name, endpoint, and credential form for each cluster.
-Reads the catalog from the current directory or --file.`,
+Reads the blueprint from the current directory or --file.`,
RunE: func(cmd *cobra.Command, args []string) error {
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
- return runClustersList(cat)
+ return runClustersList(bp)
},
}
-func runClustersList(cat *catalog.Catalog) error {
+func runClustersList(bp *blueprint.Blueprint) error {
fmt.Println()
- if !cat.IsGatewayEnabled() {
+ if !bp.IsGatewayEnabled() {
fmt.Printf(" %s gateway is not enabled\n", cmdutil.Gray("○"))
fmt.Println()
return nil
}
- clusters := cat.GatewayClusters()
+ clusters := bp.GatewayClusters()
if len(clusters) == 0 {
fmt.Printf(" %s gateway.clusters is empty — no remote clusters registered\n", cmdutil.Gray("○"))
fmt.Println()
@@ -69,9 +69,9 @@ func runClustersList(cat *catalog.Catalog) error {
var clustersValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate gateway.clusters configuration offline",
- Long: `Validate the gateway.clusters block in the catalog.
+ Long: `Validate the gateway.clusters block in the blueprint.
-Reads the catalog and checks each cluster entry for structural validity:
+Reads the blueprint and checks each cluster entry for structural validity:
endpoint required, exactly one credential form, required secret refs present.
Also verifies that every static serve.cluster and target.cluster reference
@@ -82,34 +82,34 @@ With --full, also reports which CRDs route to each cluster.`,
RunE: func(cmd *cobra.Command, args []string) error {
full, _ := cmd.Flags().GetBool("full")
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
- return runClustersValidate(cat, full)
+ return runClustersValidate(bp, full)
},
}
-func runClustersValidate(cat *catalog.Catalog, full bool) error {
+func runClustersValidate(bp *blueprint.Blueprint, full bool) error {
fmt.Println()
fmt.Printf("%s inrun clusters validate\n", cmdutil.Bold("⎈"))
fmt.Println()
- if !cat.IsGatewayEnabled() {
+ if !bp.IsGatewayEnabled() {
fmt.Printf(" %s gateway is not enabled — no clusters to validate\n", cmdutil.Gray("○"))
fmt.Println()
return nil
}
- clusters := cat.GatewayClusters()
+ clusters := bp.GatewayClusters()
if len(clusters) == 0 {
fmt.Printf(" %s gateway.clusters is empty — no remote clusters registered\n", cmdutil.Gray("○"))
fmt.Println()
return nil
}
- clusterRefs := buildClusterRefIndex(cat)
+ clusterRefs := buildClusterRefIndex(bp)
fmt.Printf(" gateway.clusters (%d registered)\n", len(clusters))
fmt.Println()
@@ -122,7 +122,7 @@ func runClustersValidate(cat *catalog.Catalog, full bool) error {
}
}
- if err := validate.ValidateGatewayClusters(cat); err != nil {
+ if err := validate.ValidateGatewayClusters(bp); err != nil {
fmt.Printf(" %s %s\n", cmdutil.FailureMark(), cmdutil.Red(err.Error()))
allOK = false
}
@@ -145,13 +145,13 @@ var clustersCheckCmd = &cobra.Command{
Use: "check",
Short: "Connect to each registered cluster and verify CRD presence",
Long: `Go online: read each cluster's credential secret from the management
-cluster, connect to the remote cluster, verify the catalog's CRDs are installed,
+cluster, connect to the remote cluster, verify the blueprint's CRDs are installed,
and report pass / unreachable / missing-CRD per cluster.
Uses the current kubectl context to reach the management cluster.
Pass --context to use a specific context instead.
-With --config , skips the catalog entirely and checks connectivity only
+With --config , skips the blueprint entirely and checks connectivity only
for each cluster listed in the bootstrap config file. Use this to verify
contexts are reachable before bootstrapping them.`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -167,12 +167,12 @@ contexts are reachable before bootstrapping them.`,
return runClustersCheck(cmd.Context(), nil, clusters, clusterCtx)
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
- all := cat.GatewayClusters()
+ all := bp.GatewayClusters()
if len(all) == 0 {
fmt.Printf(" %s gateway.clusters is empty — nothing to check\n", cmdutil.Gray("○"))
return nil
@@ -193,11 +193,11 @@ contexts are reachable before bootstrapping them.`,
clusters = selected
}
- return runClustersCheck(cmd.Context(), cat, clusters, clusterCtx)
+ return runClustersCheck(cmd.Context(), bp, clusters, clusterCtx)
},
}
-func runClustersCheck(ctx context.Context, cat *catalog.Catalog, clusters map[string]types.GatewayClusterConfig, clusterCtx string) error {
+func runClustersCheck(ctx context.Context, bp *blueprint.Blueprint, clusters map[string]types.GatewayClusterConfig, clusterCtx string) error {
fmt.Println()
fmt.Printf("%s inrun clusters check\n", cmdutil.Bold("⎈"))
fmt.Println()
@@ -207,7 +207,7 @@ func runClustersCheck(ctx context.Context, cat *catalog.Catalog, clusters map[st
return fmt.Errorf("%s management cluster: %w", cmdutil.FailureMark(), err)
}
- results := apigateway.CheckClusters(ctx, cat, clusters, localKube, "default")
+ results := apigateway.CheckClusters(ctx, bp, clusters, localKube, "default")
anyFailed := false
for _, r := range results {
@@ -308,9 +308,9 @@ func credentialSummary(cfg types.GatewayClusterConfig) string {
}
}
-func buildClusterRefIndex(cat *catalog.Catalog) map[string][]string {
+func buildClusterRefIndex(bp *blueprint.Blueprint) map[string][]string {
idx := map[string][]string{}
- for crdName, entry := range cat.EnabledCRDs() {
+ for crdName, entry := range bp.EnabledCRDs() {
if entry.Serve == nil {
continue
}
@@ -334,7 +334,7 @@ func init() {
clustersValidateCmd.Flags().Bool("full", false, "Show CRD routing references per cluster")
clustersCheckCmd.Flags().String("context", "", "kubectl context for reading credential secrets (defaults to current context)")
clustersCheckCmd.Flags().String("clusters", "", "comma-separated list of cluster names to check (default: all registered clusters)")
- clustersCheckCmd.Flags().String("config", "", "path to a cluster-config.yaml to check connectivity without a catalog")
+ clustersCheckCmd.Flags().String("config", "", "path to a cluster-config.yaml to check connectivity without a blueprint")
clustersCmd.AddCommand(clustersValidateCmd)
clustersCmd.AddCommand(clustersCheckCmd)
diff --git a/cmd/cli/cluster/clusters_bootstrap.go b/cmd/cli/cluster/clusters_bootstrap.go
index 225626773..88ecef803 100644
--- a/cmd/cli/cluster/clusters_bootstrap.go
+++ b/cmd/cli/cluster/clusters_bootstrap.go
@@ -14,7 +14,7 @@ import (
"github.com/spf13/cobra"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/tools/cluster/bootstrap"
)
@@ -26,7 +26,7 @@ cluster, then store the credentials in the gateway cluster so the gateway
can route applies to it.
The ClusterRole is scoped to exactly the serve-enabled CRDs declared in the
-catalog — no wildcard resources, no cluster-admin.
+blueprint — no wildcard resources, no cluster-admin.
Examples:
inrun clusters bootstrap --context kind-prod --name prod
@@ -68,12 +68,12 @@ Examples:
SANamespace: saNamespace,
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
- return runBootstrapSingle(cmd.Context(), cat, entry, opts)
+ return runBootstrapSingle(cmd.Context(), bp, entry, opts)
},
}
@@ -94,7 +94,7 @@ func runBootstrapValidate(path string) error {
}
// runBootstrapConfig bootstraps all clusters listed in the config file.
-// No catalog is required — rules come from each entry's rules: field (if any).
+// No blueprint is required — rules come from each entry's rules: field (if any).
// When outPath is non-empty, the bootstrap results are written to a
// gateway.clusters-shaped YAML file suitable for `inrun clusters check --config`.
func runBootstrapConfig(cmd *cobra.Command, path, outPath string, opts bootstrap.RunOptions) error {
@@ -128,7 +128,7 @@ func runBootstrapConfig(cmd *cobra.Command, path, outPath string, opts bootstrap
}
// runBootstrapSingleResult is the result-returning variant used by runBootstrapConfig.
-func runBootstrapSingleResult(ctx context.Context, cat *catalog.Catalog, entry bootstrap.ClusterEntry, opts bootstrap.RunOptions) (*bootstrap.Result, error) {
+func runBootstrapSingleResult(ctx context.Context, bp *blueprint.Blueprint, entry bootstrap.ClusterEntry, opts bootstrap.RunOptions) (*bootstrap.Result, error) {
fmt.Println()
fmt.Printf("%s inrun clusters bootstrap\n", cmdutil.Bold("⎈"))
fmt.Printf(" %s cluster name: %s\n", cmdutil.Gray("→"), cmdutil.Bold(entry.Name))
@@ -140,7 +140,7 @@ func runBootstrapSingleResult(ctx context.Context, cat *catalog.Catalog, entry b
}
fmt.Println()
- result, err := bootstrap.Cluster(ctx, cat, entry, opts, func(msg string) {
+ result, err := bootstrap.Cluster(ctx, bp, entry, opts, func(msg string) {
fmt.Printf(" %s %s\n", cmdutil.SuccessMark(), msg)
})
if err != nil {
@@ -158,12 +158,12 @@ func runBootstrapSingleResult(ctx context.Context, cat *catalog.Catalog, entry b
}
fmt.Println()
- bootstrapPrintCatalogSnippet(entry.Name, result.Endpoint, result.SecretName, result.SecretNamespace, !result.HasCA)
+ bootstrapPrintBlueprintSnippet(entry.Name, result.Endpoint, result.SecretName, result.SecretNamespace, !result.HasCA)
return result, nil
}
// runBootstrapSingle bootstraps one cluster entry and prints the appropriate output.
-func runBootstrapSingle(ctx context.Context, cat *catalog.Catalog, entry bootstrap.ClusterEntry, opts bootstrap.RunOptions) error {
+func runBootstrapSingle(ctx context.Context, bp *blueprint.Blueprint, entry bootstrap.ClusterEntry, opts bootstrap.RunOptions) error {
fmt.Println()
fmt.Printf("%s inrun clusters bootstrap\n", cmdutil.Bold("⎈"))
fmt.Printf(" %s cluster name: %s\n", cmdutil.Gray("→"), cmdutil.Bold(entry.Name))
@@ -175,7 +175,7 @@ func runBootstrapSingle(ctx context.Context, cat *catalog.Catalog, entry bootstr
}
fmt.Println()
- result, err := bootstrap.Cluster(ctx, cat, entry, opts, func(msg string) {
+ result, err := bootstrap.Cluster(ctx, bp, entry, opts, func(msg string) {
fmt.Printf(" %s %s\n", cmdutil.SuccessMark(), msg)
})
if err != nil {
@@ -193,14 +193,14 @@ func runBootstrapSingle(ctx context.Context, cat *catalog.Catalog, entry bootstr
}
fmt.Println()
- bootstrapPrintCatalogSnippet(entry.Name, result.Endpoint, result.SecretName, result.SecretNamespace, !result.HasCA)
+ bootstrapPrintBlueprintSnippet(entry.Name, result.Endpoint, result.SecretName, result.SecretNamespace, !result.HasCA)
return nil
}
// ── output helpers ────────────────────────────────────────────────────────────
-func bootstrapPrintCatalogSnippet(name, endpoint, secretName, namespace string, insecure bool) {
- fmt.Printf("%s Add to your catalog:\n\n", cmdutil.Bold("⎈"))
+func bootstrapPrintBlueprintSnippet(name, endpoint, secretName, namespace string, insecure bool) {
+ fmt.Printf("%s Add to your blueprint:\n\n", cmdutil.Bold("⎈"))
if insecure {
fmt.Printf(`gateway:
clusters:
diff --git a/cmd/cli/cmdutil/blueprint.go b/cmd/cli/cmdutil/blueprint.go
new file mode 100644
index 000000000..4c6c8736c
--- /dev/null
+++ b/cmd/cli/cmdutil/blueprint.go
@@ -0,0 +1,135 @@
+//go:build !runtime && !gateway
+
+package cmdutil
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
+ "github.com/intentd/inrun/pkg/merger"
+ "github.com/intentd/inrun/pkg/types"
+ "github.com/spf13/cobra"
+)
+
+// DefaultNamespace returns the namespace to use when --namespace is not supplied.
+// Reads INRUN_NAMESPACE from the environment so that CLI invocations inside
+// an already-configured cluster automatically target the right namespace.
+func DefaultNamespace() string {
+ if ns := os.Getenv("INRUN_NAMESPACE"); ns != "" {
+ return ns
+ }
+ return "inrun-system"
+}
+
+// ParseFilePaths handles comma-separated values and returns a slice of paths
+func ParseFilePaths(paths []string) []string {
+ var expanded []string
+ for _, p := range paths {
+ // Split by comma and trim spaces
+ parts := strings.Split(p, ",")
+ for _, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if trimmed != "" {
+ expanded = append(expanded, trimmed)
+ }
+ }
+ }
+ return expanded
+}
+
+type MergerOut struct {
+ Merger *merger.Merger
+ CRDs []types.CRDEntry
+ Blueprint *blueprint.Blueprint
+ Paths []string
+ Enabled map[string]types.CRDEntry // m.Enabled()
+}
+
+func GenerateBlueprint(cmd *cobra.Command) (*MergerOut, error) {
+ blueprintPaths, _ := cmd.Flags().GetStringSlice("file")
+
+ expanded := ParseFilePaths(blueprintPaths)
+ if len(expanded) == 0 {
+ expanded = DefaultFilePaths()
+ }
+ if len(expanded) == 0 {
+ return nil, fmt.Errorf(ErrNoBlueprint)
+ }
+
+ // Absolutize so merger.FirstEntryDir() is always absolute. Without this,
+ // relative paths cause a double-join when PopulateAPITypesFromCRDFile
+ // prepends blueprintDir to a crdFile that was already joined once during load.
+ for i, p := range expanded {
+ if abs, err := filepath.Abs(p); err == nil {
+ expanded[i] = abs
+ }
+ }
+
+ m := merger.New(expanded...)
+ if err := m.Merge(); err != nil {
+ return nil, err
+ }
+
+ var bp blueprint.Blueprint
+ bp.Spec = m.ToSpec()
+
+ // Fill apiTypes from crdFile, as the pipeline does, so generators see a
+ // fully specified CRD. The merger has already made crdFile absolute.
+ crdMap := bp.Spec.CRDs
+ if err := resolveCRDFiles(crdMap); err != nil {
+ return nil, err
+ }
+ enabled := m.Enabled()
+ if err := resolveCRDFiles(enabled); err != nil {
+ return nil, err
+ }
+
+ // Convert map to slice for generate functions
+ crds := make([]types.CRDEntry, 0, len(crdMap))
+ for _, c := range crdMap {
+ crds = append(crds, c)
+ }
+
+ return &MergerOut{
+ Merger: m,
+ CRDs: crds,
+ Blueprint: &bp,
+ Paths: blueprintPaths,
+ Enabled: enabled,
+ }, nil
+}
+
+// resolveCRDFiles fills each entry's apiTypes from its crdFile, and migrate.to
+// from migrate.to.crdFile, then clears both so nothing downstream depends on
+// a local path.
+func resolveCRDFiles(crds map[string]types.CRDEntry) error {
+ for name, crd := range crds {
+ if crd.CRDFile != "" {
+ if err := blueprint.PopulateAPITypesFromCRDFile(&crd, ""); err != nil {
+ return fmt.Errorf("CRD %q: %w", name, err)
+ }
+ crd.CRDFile = ""
+ }
+ if crd.Migrate != nil && crd.Migrate.To.CRDFile != "" {
+ if err := blueprint.PopulateMigrateTargetFromCRDFile(&crd, ""); err != nil {
+ return fmt.Errorf("CRD %q: %w", name, err)
+ }
+ crd.Migrate.To.CRDFile = ""
+ }
+ crds[name] = crd
+ }
+ return nil
+}
+
+// BuildBlueprint builds the expanded Blueprint using the --file flag from cmd.
+func BuildBlueprint(cmd *cobra.Command) (*blueprint.Blueprint, error) {
+ m, err := GenerateBlueprint(cmd)
+ if err != nil {
+ return nil, fmt.Errorf("generating Blueprint: %w", err)
+ }
+ return pipeline.BuildExpanded(Cfg, m.Merger)
+}
diff --git a/cmd/cli/cmdutil/catalog.go b/cmd/cli/cmdutil/catalog.go
deleted file mode 100644
index 092c4ecc6..000000000
--- a/cmd/cli/cmdutil/catalog.go
+++ /dev/null
@@ -1,129 +0,0 @@
-//go:build !runtime && !gateway
-
-package cmdutil
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
- "github.com/intentd/inrun/pkg/merger"
- "github.com/intentd/inrun/pkg/types"
- "github.com/spf13/cobra"
-)
-
-// DefaultNamespace returns the namespace to use when --namespace is not supplied.
-// Reads INRUN_NAMESPACE from the environment so that CLI invocations inside
-// an already-configured cluster automatically target the right namespace.
-func DefaultNamespace() string {
- if ns := os.Getenv("INRUN_NAMESPACE"); ns != "" {
- return ns
- }
- return "inrun-system"
-}
-
-// ParseFilePaths handles comma-separated values and returns a slice of paths
-func ParseFilePaths(paths []string) []string {
- var expanded []string
- for _, p := range paths {
- // Split by comma and trim spaces
- parts := strings.Split(p, ",")
- for _, part := range parts {
- trimmed := strings.TrimSpace(part)
- if trimmed != "" {
- expanded = append(expanded, trimmed)
- }
- }
- }
- return expanded
-}
-
-type MergerOut struct {
- Merger *merger.Merger
- CRDs []types.CRDEntry
- Catalog *catalog.Catalog
- Paths []string
- Enabled map[string]types.CRDEntry // m.Enabled()
-}
-
-func GenerateCatalog(cmd *cobra.Command) (*MergerOut, error) {
- catalogPaths, _ := cmd.Flags().GetStringSlice("file")
-
- expanded := ParseFilePaths(catalogPaths)
- if len(expanded) == 0 {
- expanded = DefaultFilePaths()
- }
- if len(expanded) == 0 {
- return nil, fmt.Errorf(ErrNoCatalog)
- }
-
- // Absolutize so merger.FirstEntryDir() is always absolute. Without this,
- // relative paths cause a double-join when PopulateAPITypesFromCRDFile
- // prepends catalogDir to a crdFile that was already joined once during load.
- for i, p := range expanded {
- if abs, err := filepath.Abs(p); err == nil {
- expanded[i] = abs
- }
- }
-
- m := merger.New(expanded...)
- if err := m.Merge(); err != nil {
- return nil, err
- }
-
- var cat catalog.Catalog
- cat.Spec = m.ToSpec()
-
- // Fill apiTypes from crdFile, as the pipeline does, so generators see a
- // fully specified CRD. The merger has already made crdFile absolute.
- crdMap := cat.Spec.CRDs
- if err := resolveCRDFiles(crdMap); err != nil {
- return nil, err
- }
- enabled := m.Enabled()
- if err := resolveCRDFiles(enabled); err != nil {
- return nil, err
- }
-
- // Convert map to slice for generate functions
- crds := make([]types.CRDEntry, 0, len(crdMap))
- for _, c := range crdMap {
- crds = append(crds, c)
- }
-
- return &MergerOut{
- Merger: m,
- CRDs: crds,
- Catalog: &cat,
- Paths: catalogPaths,
- Enabled: enabled,
- }, nil
-}
-
-// resolveCRDFiles fills each entry's apiTypes from its crdFile and clears
-// crdFile, so nothing downstream depends on a local path.
-func resolveCRDFiles(crds map[string]types.CRDEntry) error {
- for name, crd := range crds {
- if crd.CRDFile == "" {
- continue
- }
- if err := catalog.PopulateAPITypesFromCRDFile(&crd, ""); err != nil {
- return fmt.Errorf("CRD %q: %w", name, err)
- }
- crd.CRDFile = ""
- crds[name] = crd
- }
- return nil
-}
-
-// BuildCatalog builds the expanded Catalog using the --file flag from cmd.
-func BuildCatalog(cmd *cobra.Command) (*catalog.Catalog, error) {
- m, err := GenerateCatalog(cmd)
- if err != nil {
- return nil, fmt.Errorf("generating Catalog: %w", err)
- }
- return pipeline.BuildExpanded(Cfg, m.Merger)
-}
diff --git a/cmd/cli/cmdutil/constants.go b/cmd/cli/cmdutil/constants.go
index 6bf3feb1c..b727a421a 100644
--- a/cmd/cli/cmdutil/constants.go
+++ b/cmd/cli/cmdutil/constants.go
@@ -3,7 +3,7 @@
package cmdutil
const (
- SchemaRefCatalog = "https://inrun.dev/docs/reference/schema/catalog/"
+ SchemaRefBlueprint = "https://inrun.dev/docs/reference/schema/blueprint/"
SchemaRefStack = "https://inrun.dev/docs/reference/schema/stack/"
SchemaRefModule = "https://inrun.dev/docs/reference/schema/module/"
SchemaRefE2E = "https://inrun.dev/docs/reference/schema/e2e/"
diff --git a/cmd/cli/cmdutil/crs.go b/cmd/cli/cmdutil/crs.go
index 481977322..936cf01b6 100644
--- a/cmd/cli/cmdutil/crs.go
+++ b/cmd/cli/cmdutil/crs.go
@@ -64,7 +64,7 @@ type CRSimInputs struct {
// The FIRST document of the target kind is the CR under test — unchanged
// behavior from a single-CR file. Any FURTHER documents of that same kind
// are pre-existing instances: seeded into the fake dynamic client so
-// reconcile-time checks that list other instances (operator: unique) can
+// reconcile-time checks that list other instances (unique: true) can
// see them, but never reconciled themselves — same convention already used
// for cross: peers, extended from "one doc per other kind" to "one or more
// docs of the CRD's own kind". Documents of OTHER kinds become cross: peers,
diff --git a/cmd/cli/cmdutil/doc.go b/cmd/cli/cmdutil/doc.go
index f0ad1be06..2ec593db7 100644
--- a/cmd/cli/cmdutil/doc.go
+++ b/cmd/cli/cmdutil/doc.go
@@ -1,4 +1,4 @@
// Package cmdutil holds what every command shares: the root command and
-// Execute, loading a Catalog from --file, reading CR files, output helpers
-// and aliases for the general-purpose helpers in pkg/utils.
+// Execute, loading a Blueprint from --file, reading CR files, output helpers
+// and aliases for the helpers in pkg/util/term and pkg/util/fileio.
package cmdutil
diff --git a/cmd/cli/cmdutil/files.go b/cmd/cli/cmdutil/files.go
index 2b59a06f2..00f786391 100644
--- a/cmd/cli/cmdutil/files.go
+++ b/cmd/cli/cmdutil/files.go
@@ -5,51 +5,44 @@ import (
"os"
"path/filepath"
- "github.com/intentd/inrun/pkg/utils"
+ "github.com/intentd/inrun/pkg/registry"
+ "github.com/intentd/inrun/pkg/util/fileio"
)
const (
- FileCatalog = "catalog.yaml"
- fileStack = "stack.yaml"
- FileE2e = "e2e.yaml"
- FileSimulate = "simulate.yaml"
- FileCrd = "crd.yaml"
- FileCr = "cr.yaml"
+ FileBlueprint = registry.FileBlueprint
+ fileStack = registry.FileStack
+ FileE2e = registry.FileE2E
+ FileSimulate = registry.FileSimulate
+ FileCrd = registry.FileCRD
+ FileCr = registry.FileCR
FileReadMe = "README.md"
FileMakeFile = "Makefile"
FileDockerfile = "Dockerfile"
FileValues = "values.yaml"
- // A project keeps catalog.yaml at its root, what it applies to the cluster
+ // A project keeps blueprint.yaml at its root, what it applies to the cluster
// (crd.yaml, cr.yaml, setup files) in manifests/, and its simulate.yaml,
// e2e.yaml and test values in test/.
- DirManifests = "manifests"
- DirTest = "test"
+ DirManifests = registry.DirManifests
+ DirTest = registry.DirTest
)
-// FindFile returns name if it exists in the current directory, else the
-// first dirs/name that exists, else "".
-func FindFile(name string, dirs ...string) string {
- if FileExists(name) {
- return name
- }
- for _, d := range dirs {
- if p := filepath.Join(d, name); FileExists(p) {
- return p
- }
- }
- return ""
+// FindFile returns where name is in the project in the current directory:
+// its root, else its place in the standard layout (manifests/ or test/), else "".
+func FindFile(name string) string {
+ return registry.FindPatternFile(".", name)
}
-// ResolveCatalogPaths resolves the catalog file paths in the following order:
+// ResolveBlueprintPaths resolves the blueprint file paths in the following order:
//
// 1. Explicit CLI paths (highest priority)
-// 2. Default file paths in the working directory (catalog.yaml, stack.yaml, etc.)
-// 3. Paths defined in the Config (cfg.Catalog().Paths())
+// 2. Default file paths in the working directory (blueprint.yaml, stack.yaml, etc.)
+// 3. Paths defined in the Config (cfg.Blueprint().Paths())
//
-// Returns an error if no catalog file can be resolved.
-func ResolveCatalogPaths(cliPaths []string) ([]string, error) {
- cfgPaths := Cfg.Catalog().Paths()
+// Returns an error if no blueprint file can be resolved.
+func ResolveBlueprintPaths(cliPaths []string) ([]string, error) {
+ cfgPaths := Cfg.Blueprint().Paths()
// 1. CLI-provided paths — convert to absolute so all downstream relative
// path resolutions (crdFile, crFiles, setup, Stack imports) use the
@@ -76,14 +69,14 @@ func ResolveCatalogPaths(cliPaths []string) ([]string, error) {
return cfgPaths, nil
}
- return nil, fmt.Errorf(ErrNoCatalog)
+ return nil, fmt.Errorf(ErrNoBlueprint)
}
-// DefaultFilePaths returns the default catalog file if one exists in the
-// current directory and no -f flag was provided. Tries catalog.yaml first,
+// DefaultFilePaths returns the default blueprint file if one exists in the
+// current directory and no -f flag was provided. Tries blueprint.yaml first,
// then stack.yaml — the same precedence as Docker's Dockerfile / compose.yaml.
func DefaultFilePaths() []string {
- for _, name := range []string{FileCatalog, fileStack} {
+ for _, name := range []string{FileBlueprint, fileStack} {
if _, err := os.Stat(name); err == nil {
return []string{name}
}
@@ -91,20 +84,20 @@ func DefaultFilePaths() []string {
return nil
}
-const ErrNoCatalog = "no catalog.yaml or stack.yaml found in current directory\n" +
+const ErrNoBlueprint = "no blueprint.yaml or stack.yaml found in current directory\n" +
"pass -f or create one with inrun init"
-// ResolveCatalogFile resolves a single catalog file path from a CLI flag value.
+// ResolveBlueprintFile resolves a single blueprint file path from a CLI flag value.
// If flagValue is empty it falls back to defaultFilePaths(). The resolved path
// is always returned as an absolute path.
-func ResolveCatalogFile(flagValue string) (string, error) {
+func ResolveBlueprintFile(flagValue string) (string, error) {
if flagValue == "" {
if d := DefaultFilePaths(); len(d) > 0 {
flagValue = d[0]
}
}
if flagValue == "" {
- return "", fmt.Errorf(ErrNoCatalog)
+ return "", fmt.Errorf(ErrNoBlueprint)
}
if abs, err := filepath.Abs(flagValue); err == nil {
return abs, nil
@@ -112,8 +105,33 @@ func ResolveCatalogFile(flagValue string) (string, error) {
return flagValue, nil
}
-// File helpers live in pkg/utils; these names keep existing callers unchanged.
var (
- FileExists = utils.FileExists
- IsDir = utils.IsDir
+ FileExists = fileio.FileExists
+ IsDir = fileio.IsDir
)
+
+// ChartDir returns a --chart flag value as an absolute path, or "" when it is
+// empty. The path must be a Helm chart directory: e2e may change directory
+// before installing.
+func ChartDir(chart string) (string, error) {
+ if chart == "" {
+ return "", nil
+ }
+ abs, err := filepath.Abs(chart)
+ if err != nil {
+ return "", fmt.Errorf("--chart %q: %w", chart, err)
+ }
+ if _, err := os.Stat(filepath.Join(abs, "Chart.yaml")); err != nil {
+ return "", fmt.Errorf("--chart %q: not a Helm chart directory (no Chart.yaml)", chart)
+ }
+ return abs, nil
+}
+
+// HelmSetArgs turns --set flag values into helm arguments: "--set", value, …
+func HelmSetArgs(sets []string) []string {
+ var args []string
+ for _, s := range sets {
+ args = append(args, "--set", s)
+ }
+ return args
+}
diff --git a/cmd/cli/cmdutil/files_dev.go b/cmd/cli/cmdutil/files_dev.go
index e5524c155..614181b68 100644
--- a/cmd/cli/cmdutil/files_dev.go
+++ b/cmd/cli/cmdutil/files_dev.go
@@ -13,10 +13,10 @@ import (
"github.com/intentd/inrun/pkg/registry"
)
-// ResolveOCIRunPath resolves an OCI pattern reference to a local catalog or
+// ResolveOCIRunPath resolves an OCI pattern reference to a local blueprint or
// stack path, pulling to the cache if necessary.
func ResolveOCIRunPath(ctx context.Context, ref string, useStack, refresh bool) (string, error) {
- r, err := registry.ResolveForKind(ref, registry.CatalogKind)
+ r, err := registry.ResolveForKind(ref, registry.BlueprintKind)
if err != nil {
return "", fmt.Errorf("invalid reference: %w", err)
}
@@ -41,7 +41,7 @@ func ResolveOCIRunPath(ctx context.Context, ref string, useStack, refresh bool)
return "", err
}
- target := FileCatalog
+ target := FileBlueprint
if useStack {
target = fileStack
}
diff --git a/cmd/cli/cmdutil/helper.go b/cmd/cli/cmdutil/helper.go
index d305d1ec8..4e52785f0 100644
--- a/cmd/cli/cmdutil/helper.go
+++ b/cmd/cli/cmdutil/helper.go
@@ -14,10 +14,14 @@ import (
"strings"
"time"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/util/fileio"
+ "github.com/intentd/inrun/pkg/util/kube"
+ "github.com/intentd/inrun/pkg/util/structured"
+ "github.com/intentd/inrun/pkg/util/term"
+
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/registry"
"github.com/intentd/inrun/pkg/types"
- "github.com/intentd/inrun/pkg/utils"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@@ -28,61 +32,61 @@ import (
var (
// colors / styles
- Gray = utils.Gray
- Bold = utils.Bold
- Dim = utils.Dim
- Cyan = utils.Cyan
- Green = utils.Green
- Blue = utils.Blue
- White = utils.White
- Yellow = utils.Yellow
- Red = utils.Red
- magenta = utils.Magenta
+ Gray = term.Gray
+ Bold = term.Bold
+ Dim = term.Dim
+ Cyan = term.Cyan
+ Green = term.Green
+ Blue = term.Blue
+ White = term.White
+ Yellow = term.Yellow
+ Red = term.Red
+ magenta = term.Magenta
// marks and icons
- SuccessMark = utils.SuccessMark
- FailureMark = utils.FailureMark
- WarningMark = utils.WarningMark
- InfoMark = utils.InfoMark
- SecureMark = utils.SecureMark
- SomeSecureMark = utils.SomeSecureMark
- NoSecurityMark = utils.NoSecurityMark
- HealthIcon = utils.HealthIcon
- HealthIconReady = utils.HealthIconReady
- HealthIconWarn = utils.HealthIconWarning
- HealthIconInfo = utils.HealthIconInfo
+ SuccessMark = term.SuccessMark
+ FailureMark = term.FailureMark
+ WarningMark = term.WarningMark
+ InfoMark = term.InfoMark
+ SecureMark = term.SecureMark
+ SomeSecureMark = term.SomeSecureMark
+ NoSecurityMark = term.NoSecurityMark
+ HealthIcon = term.HealthIcon
+ HealthIconReady = term.HealthIconReady
+ HealthIconWarn = term.HealthIconWarning
+ HealthIconInfo = term.HealthIconInfo
// other cli utilities
- IsRunningInPod = utils.IsRunningInPod
- WriteFileAndFormat = utils.WriteFileAndFormat
- SplitCommaSeparated = utils.SplitCommaSeparated
- ReadLocal = utils.ReadLocal
- StrictUnmarshal = utils.StrictUnmarshal
- PruneEmptyYAML = utils.PruneEmptyYAML
- CopyFile = utils.CopyFile
- CopyDir = utils.CopyDir
- JoinPath = utils.JoinRelative
- FormatSize = utils.FormatSize
- WordWrap = utils.WordWrap
- visibleLen = utils.VisibleLen
- PadRight = utils.PadRight
- OrDefault = utils.OrDefault
- ContainsTag = utils.ContainsFold
- IsTerminal = utils.IsTerminal
- HumanDuration = utils.FormatDuration
- DumpCRsJSON = utils.DumpCRsJSON
- DumpCRsYAML = utils.DumpCRsYAML
+ IsRunningInPod = kube.IsRunningInPod
+ WriteFileAndFormat = structured.WriteFileAndFormat
+ SplitCommaSeparated = structured.SplitCommaSeparated
+ ReadLocal = fileio.ReadLocal
+ StrictUnmarshal = structured.StrictUnmarshal
+ PruneEmptyYAML = structured.PruneEmptyYAML
+ CopyFile = fileio.CopyFile
+ CopyDir = fileio.CopyDir
+ JoinPath = fileio.JoinRelative
+ FormatSize = term.FormatSize
+ WordWrap = term.WordWrap
+ visibleLen = term.VisibleLen
+ PadRight = term.PadRight
+ OrDefault = term.OrDefault
+ ContainsTag = term.ContainsFold
+ IsTerminal = term.IsTerminal
+ HumanDuration = term.FormatDuration
+ DumpCRsJSON = kube.DumpCRsJSON
+ DumpCRsYAML = kube.DumpCRsYAML
)
// SortedKeys returns a sorted slice of all keys from the given map.
func SortedKeys[V any](m map[string]V) []string {
- return utils.SortedKeys(m)
+ return structured.SortedKeys(m)
}
// StartSpinner starts a terminal progress spinner with the given message.
// Call Success, Failure, or Stop on the returned value when done.
-func StartSpinner(msg string) *utils.Spinner {
- return utils.StartSpinner(msg)
+func StartSpinner(msg string) *term.Spinner {
+ return term.StartSpinner(msg)
}
// Shadow global command flags
@@ -128,18 +132,18 @@ func ShadowGlobalCommandFlags(cmd *cobra.Command, flags ...string) {
// PrintTemplateSummary prints the human-readable default output of `inrun template`.
// CRDs are listed in startup order so the user sees the dependency sequence.
-func PrintTemplateSummary(cat *catalog.Catalog, crds map[string]types.CRDEntry, startupOrder []string) {
- meta := cat.Metadata()
+func PrintTemplateSummary(bp *blueprint.Blueprint, crds map[string]types.CRDEntry, startupOrder []string) {
+ meta := bp.Metadata()
- fmt.Printf("\n%s", Cyan(Bold("Catalog")))
+ fmt.Printf("\n%s", Cyan(Bold("Blueprint")))
if meta.Name != "" {
fmt.Printf(": %s", Bold(meta.Name))
}
if meta.Version != "" {
fmt.Printf(" %s", Dim("("+meta.Version+")"))
}
- if cat.APIVersion != "" {
- fmt.Printf(" %s", Dim(cat.APIVersion))
+ if bp.APIVersion != "" {
+ fmt.Printf(" %s", Dim(bp.APIVersion))
}
fmt.Println()
@@ -238,8 +242,8 @@ func PrintTemplateSummary(cat *catalog.Catalog, crds map[string]types.CRDEntry,
// Autoscale
if crd.AutoscaleEnabled() {
a := crd.Box().EffectiveAutoscale()
- if a.Profile != "" {
- fmt.Printf("%s%s profile=%s\n", indent, magenta("autoscale:"), a.Profile)
+ if a.Preset != "" {
+ fmt.Printf("%s%s preset=%s\n", indent, magenta("autoscale:"), a.Preset)
} else {
parts := []string{}
if len(a.Conditions.When) > 0 {
@@ -266,7 +270,7 @@ func PrintTemplateSummary(cat *catalog.Catalog, crds map[string]types.CRDEntry,
fmt.Println()
}
- fmt.Printf(" %s\n\n", Green("✓ Catalog is valid"))
+ fmt.Printf(" %s\n\n", Green("✓ Blueprint is valid"))
}
// ── printDependencyGraph ──────────────────────────────────────────────────────
@@ -274,7 +278,7 @@ func PrintTemplateSummary(cat *catalog.Catalog, crds map[string]types.CRDEntry,
// PrintDependencyGraph prints a two-part dependency view:
// 1. Ordered startup list (flat)
// 2. Tree view showing the dependency hierarchy
-func PrintDependencyGraph(crds map[string]types.CRDEntry, g *catalog.DependencyGraph, startupOrder []string) {
+func PrintDependencyGraph(crds map[string]types.CRDEntry, g *blueprint.DependencyGraph, startupOrder []string) {
fmt.Printf("\n%s\n\n", Cyan(Bold("Dependency Graph")))
// ── Part 1: startup order list ────────────────────────────────────────────
@@ -333,7 +337,7 @@ func PrintDependencyGraph(crds map[string]types.CRDEntry, g *catalog.DependencyG
}
// printGraphNode recursively prints one CRD node and its dependents in tree form.
-func printGraphNode(crds map[string]types.CRDEntry, g *catalog.DependencyGraph, name, indent, connector string, printed map[string]bool) {
+func printGraphNode(crds map[string]types.CRDEntry, g *blueprint.DependencyGraph, name, indent, connector string, printed map[string]bool) {
crd, ok := crds[name]
if !ok {
return
@@ -378,7 +382,7 @@ func printGraphNode(crds map[string]types.CRDEntry, g *catalog.DependencyGraph,
// PrintCRDDetail prints the full expanded state of a single CRD as a
// human-readable document — what the runtime will use for this CRD.
-func PrintCRDDetail(crd types.CRDEntry, g *catalog.DependencyGraph) {
+func PrintCRDDetail(crd types.CRDEntry, g *blueprint.DependencyGraph) {
fmt.Printf("\n%s\n", Bold(crd.Name))
fmt.Printf(" %s %s/%s\n", Cyan("APIVersion:"), crd.APITypes.Group, crd.APITypes.Version)
fmt.Printf(" %s %s (plural: %s)\n", Cyan("Kind: "), crd.APITypes.Kind, crd.APITypes.Plural)
@@ -440,8 +444,8 @@ func PrintCRDDetail(crd types.CRDEntry, g *catalog.DependencyGraph) {
if crd.AutoscaleEnabled() {
a := crd.Box().EffectiveAutoscale()
fmt.Printf(" %s\n", magenta(Bold("Autoscale")))
- if a.Profile != "" {
- fmt.Printf(" Profile: %s\n", magenta(a.Profile))
+ if a.Preset != "" {
+ fmt.Printf(" Preset: %s\n", magenta(a.Preset))
} else {
fmt.Printf(" Interval: %s\n", a.EffectiveInterval())
fmt.Printf(" Cooldown: %s\n", a.EffectiveCooldown())
@@ -721,7 +725,7 @@ func PrintTypedBuildSteps(hasMakefile bool) {
// PrintTypedOperatorHint is called when a registry-sourced typed operator fails
// validation or simulate. Tells the user to pull the pattern, build the custom
// runtime, then re-run the same command.
-func PrintTypedOperatorHint(err *catalog.TypedOperatorError, command string) {
+func PrintTypedOperatorHint(err *blueprint.TypedOperatorError, command string) {
fmt.Printf("\n%s This operator is typed — requires a custom runtime.\n\n", Yellow("⚠"))
fmt.Printf(" Pull and build, then re-run:\n")
fmt.Printf(" inrun pull %s -o .\n", err.Ref)
@@ -731,14 +735,14 @@ func PrintTypedOperatorHint(err *catalog.TypedOperatorError, command string) {
// ── crdModeLabel ──────────────────────────────────────────────────────────────
-// PrintCatalogDeprecation prints the deprecation notice for a locally validated
-// catalog. Reads timeline state from the CatalogDeprecation methods.
+// PrintBlueprintDeprecation prints the deprecation notice for a locally validated
+// blueprint. Reads timeline state from the BlueprintDeprecation methods.
// Prints nothing if the block is nil or today is before timeline.from.
-func PrintCatalogDeprecation(d *types.CatalogDeprecation) {
- PrintCatalogDeprecationWithHint(d, "")
+func PrintBlueprintDeprecation(d *types.BlueprintDeprecation) {
+ PrintBlueprintDeprecationWithHint(d, "")
}
-func PrintCatalogDeprecationWithHint(d *types.CatalogDeprecation, hint string) {
+func PrintBlueprintDeprecationWithHint(d *types.BlueprintDeprecation, hint string) {
if d == nil {
return
}
@@ -756,7 +760,7 @@ func PrintPatternDeprecation(dep *registry.PatternDeprecated) {
if dep == nil {
return
}
- d := &types.CatalogDeprecation{
+ d := &types.BlueprintDeprecation{
MigratedTo: dep.MigratedTo,
Message: dep.Message,
}
@@ -766,7 +770,7 @@ func PrintPatternDeprecation(dep *registry.PatternDeprecated) {
To: dep.TimelineTo,
}
}
- PrintCatalogDeprecation(d)
+ PrintBlueprintDeprecation(d)
}
// printDeprecationBlock renders the deprecation block for a given state.
diff --git a/cmd/cli/cmdutil/root.go b/cmd/cli/cmdutil/root.go
index 9b8dc174e..3da5a32e2 100644
--- a/cmd/cli/cmdutil/root.go
+++ b/cmd/cli/cmdutil/root.go
@@ -5,11 +5,12 @@ import (
"os"
"path/filepath"
+ "github.com/intentd/inrun/pkg/util/term"
+
"github.com/spf13/cobra"
"github.com/intentd/inrun/pkg/config"
"github.com/intentd/inrun/pkg/logger"
- "github.com/intentd/inrun/pkg/utils"
)
var (
@@ -27,8 +28,8 @@ func Execute(k *config.Config, c context.Context) {
Cfg = k
Ctx = c
- if err := RootCmd.Execute(); err != nil {
- utils.Exit(err)
+ if err := RootCmd.ExecuteContext(c); err != nil {
+ term.Exit(err)
}
}
@@ -43,7 +44,7 @@ func init() {
// Global flags — always present in both runtime and dev builds
RootCmd.PersistentFlags().Bool("debug", false, "Enable debug logging")
- RootCmd.PersistentFlags().StringSliceP("file", "f", nil, "Path(s) or URL(s) to catalog.yaml (repeatable)")
+ RootCmd.PersistentFlags().StringSliceP("file", "f", nil, "Path(s) or URL(s) to blueprint.yaml (repeatable)")
// Dev-only flags (--kubeconfig, --verbose) and required-flag marking for
// dev commands are registered in root_dev.go (//go:build !runtime).
}
diff --git a/cmd/cli/commands_dev.go b/cmd/cli/commands_dev.go
index 137620877..d49c15f87 100644
--- a/cmd/cli/commands_dev.go
+++ b/cmd/cli/commands_dev.go
@@ -5,11 +5,12 @@ package cli
import (
_ "github.com/intentd/inrun/cmd/cli/cluster"
_ "github.com/intentd/inrun/cmd/cli/console"
+ _ "github.com/intentd/inrun/cmd/cli/functions"
_ "github.com/intentd/inrun/cmd/cli/gate"
_ "github.com/intentd/inrun/cmd/cli/generate"
_ "github.com/intentd/inrun/cmd/cli/migrate"
- _ "github.com/intentd/inrun/cmd/cli/notes"
_ "github.com/intentd/inrun/cmd/cli/registry"
+ _ "github.com/intentd/inrun/cmd/cli/rewrite"
_ "github.com/intentd/inrun/cmd/cli/run"
_ "github.com/intentd/inrun/cmd/cli/scaffold"
_ "github.com/intentd/inrun/cmd/cli/self"
diff --git a/cmd/cli/console/console.go b/cmd/cli/console/console.go
index e41decc88..73c1485c6 100644
--- a/cmd/cli/console/console.go
+++ b/cmd/cli/console/console.go
@@ -32,15 +32,15 @@ func init() {
controlCmd.Flags().StringVarP(&consolePort, "port", "p", "8081", "Port to run the Console on")
controlCmd.Flags().BoolVarP(&consoleIgnoreDefault, "ignore-default", "i", false, "Do not add the default localhost:8080 URL; start with no instances")
controlCmd.Flags().StringVarP(&consoleURLs, "urls", "u", "http://localhost:8080", "Comma-separated list of Inrun runtime URLs")
- controlCmd.Flags().StringVar(&consoleRefresh, "refresh", "10s", "Refresh interval for fetching Catalogs")
+ controlCmd.Flags().StringVar(&consoleRefresh, "refresh", "10s", "Refresh interval for fetching Blueprints")
controlCmd.Flags().StringVar(&consoleLogLevel, "log-level", "info", "Log level (debug, info, warn, error)")
controlCmd.AddCommand(controlVersionCmd)
cmdutil.RootCmd.AddCommand(controlCmd)
// Shadow global flags so they don't appear under `inrun console`
- controlCmd.Flags().StringSlice("catalog", nil, "")
- controlCmd.Flags().MarkHidden("catalog")
+ controlCmd.Flags().StringSlice("blueprint", nil, "")
+ controlCmd.Flags().MarkHidden("blueprint")
cmdutil.ShadowGlobalCommandFlags(controlCmd)
}
diff --git a/cmd/cli/functions/functions.go b/cmd/cli/functions/functions.go
new file mode 100644
index 000000000..d397baf17
--- /dev/null
+++ b/cmd/cli/functions/functions.go
@@ -0,0 +1,204 @@
+//go:build !runtime && !gateway
+
+// package function implements the functions command for browsing and searching the
+// template functions available in Blueprint expressions.
+package function
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "text/tabwriter"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+
+ "github.com/intentd/inrun/pkg/merger"
+ "github.com/intentd/inrun/pkg/template/funcs"
+ "github.com/intentd/inrun/pkg/types"
+ "github.com/intentd/kel"
+ "github.com/spf13/cobra"
+)
+
+var functionsCmd = &cobra.Command{
+ Use: "functions",
+ Short: "Browse and search Inrun functions",
+ Long: `List, search, and inspect Inrun functions — the template functions available
+in every Blueprint expression.
+
+ inrun functions list all functions
+ inrun functions --domain strings filter by domain
+ inrun functions search full-text search
+ inrun functions show show full detail for one function`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ domain, _ := cmd.Flags().GetString("domain")
+ noPager, _ := cmd.Flags().GetBool("no-pager")
+ files, _ := cmd.Flags().GetStringSlice("file")
+ expanded := cmdutil.ParseFilePaths(files)
+
+ // When -f is provided, show user-defined functions from the Blueprint.
+ if len(expanded) > 0 {
+ m := merger.New(expanded...)
+ if err := m.Merge(); err != nil {
+ return fmt.Errorf("loading blueprint: %w", err)
+ }
+ reg := m.ToFunctions()
+ if reg.Empty() {
+ fmt.Println("No user-defined functions declared in this Blueprint.")
+ return nil
+ }
+ return printUserFunctionTable(reg, noPager)
+ }
+
+ var fns []kel.FunctionInfo
+ if domain != "" {
+ fns = funcs.Registry().ByDomain(domain)
+ if len(fns) == 0 {
+ return fmt.Errorf("no functions found for domain %q — run `inrun functions` to see available domains", domain)
+ }
+ } else {
+ fns = funcs.Registry().Functions
+ }
+ return printFunctionTable(fns, noPager)
+ },
+}
+
+var functionsSearchCmd = &cobra.Command{
+ Use: "search ",
+ Short: "Search functions by name, description, or example",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ noPager, _ := cmd.Flags().GetBool("no-pager")
+ results := funcs.Registry().Search(args[0])
+ if len(results) == 0 {
+ fmt.Printf("no functions match %q\n", args[0])
+ return nil
+ }
+ fmt.Printf("Found %d function(s) matching %q:\n\n", len(results), args[0])
+ return printFunctionTable(results, noPager)
+ },
+}
+
+var functionsShowCmd = &cobra.Command{
+ Use: "show ",
+ Short: "Show full documentation for a single function",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ noPager, _ := cmd.Flags().GetBool("no-pager")
+ n, ok := funcs.Registry().Get(args[0])
+ if !ok {
+ return fmt.Errorf("function %q not found — run `inrun functions search %s` to find similar functions", args[0], args[0])
+ }
+ return printFunctionDetail(n, noPager)
+ },
+}
+
+var functionsDomainCmd = &cobra.Command{
+ Use: "domains",
+ Short: "List available function domains",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ reg := funcs.Registry()
+ fmt.Println("Available domains:")
+ fmt.Println()
+ for _, d := range reg.Domains {
+ fmt.Printf(" %-16s %-22s %d functions\n", d.Name, d.Title, len(reg.ByDomain(d.Name)))
+ }
+ return nil
+ },
+}
+
+func init() {
+ functionsCmd.Flags().StringP("domain", "d", "", "Filter by domain (e.g. strings, cron, kubernetes)")
+ functionsCmd.Flags().Bool("no-pager", false, "Print directly without paging")
+ functionsSearchCmd.Flags().Bool("no-pager", false, "Print directly without paging")
+ functionsShowCmd.Flags().Bool("no-pager", false, "Print directly without paging")
+
+ functionsCmd.AddCommand(functionsSearchCmd)
+ functionsCmd.AddCommand(functionsShowCmd)
+ functionsCmd.AddCommand(functionsDomainCmd)
+ cmdutil.RootCmd.AddCommand(functionsCmd)
+
+ // Shadow global flags
+ cmdutil.ShadowGlobalCommandFlags(functionsCmd)
+}
+
+// ── Rendering ─────────────────────────────────────────────────────────────────
+
+func printFunctionTable(functions []kel.FunctionInfo, noPager bool) error {
+ var sb strings.Builder
+ w := tabwriter.NewWriter(&sb, 0, 0, 3, ' ', 0)
+ fmt.Fprintln(w, "DOMAIN\tNAME\tDESCRIPTION")
+ fmt.Fprintln(w, "──────\t────\t───────────")
+ for _, n := range functions {
+ desc := n.Description
+ if len(desc) > 72 {
+ desc = desc[:69] + "..."
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\n", n.Domain, n.Name, desc)
+ }
+ w.Flush()
+ return page(sb.String(), noPager)
+}
+
+func printFunctionDetail(n kel.FunctionInfo, noPager bool) error {
+ sep := strings.Repeat("─", 64)
+ var sb strings.Builder
+ fmt.Fprintln(&sb, sep)
+ fmt.Fprintf(&sb, " %s\n", n.Name)
+ fmt.Fprintln(&sb, sep)
+ fmt.Fprintf(&sb, " Domain: %s\n", n.Domain)
+ if n.Description != "" {
+ fmt.Fprintf(&sb, " Description: %s\n", n.Description)
+ }
+ if len(n.Keywords) > 0 {
+ fmt.Fprintf(&sb, " Keywords: %s\n", strings.Join(n.Keywords, ", "))
+ }
+ if len(n.SeeAlso) > 0 {
+ fmt.Fprintf(&sb, " See also: %s\n", strings.Join(n.SeeAlso, ", "))
+ }
+ if n.Example != "" {
+ fmt.Fprintln(&sb)
+ fmt.Fprintln(&sb, " Example:")
+ for _, line := range strings.Split(n.Example, "\n") {
+ fmt.Fprintf(&sb, " %s\n", line)
+ }
+ }
+ fmt.Fprintln(&sb, sep)
+ return page(sb.String(), noPager)
+}
+
+// page writes output through `less` when stdout is a terminal, otherwise prints directly.
+func page(content string, noPager bool) error {
+ if noPager || !cmdutil.IsTerminal() {
+ fmt.Print(content)
+ return nil
+ }
+ pager := exec.Command("less", "-RFX")
+ pager.Stdin = strings.NewReader(content)
+ pager.Stdout = os.Stdout
+ pager.Stderr = os.Stderr
+ if err := pager.Run(); err != nil {
+ fmt.Print(content)
+ }
+ return nil
+}
+
+func printUserFunctionTable(reg types.KELFunctionRegistry, noPager bool) error {
+ var sb strings.Builder
+ w := tabwriter.NewWriter(&sb, 0, 0, 3, ' ', 0)
+ fmt.Fprintln(w, "NAME\tEXPRESSION\tDESCRIPTION")
+ fmt.Fprintln(w, "────\t──────────\t───────────")
+ for _, n := range reg.Definitions {
+ expr := n.Expression
+ if len(expr) > 60 {
+ expr = expr[:57] + "..."
+ }
+ desc := n.Description
+ if len(desc) > 50 {
+ desc = desc[:47] + "..."
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\n", n.Name, expr, desc)
+ }
+ w.Flush()
+ return page(sb.String(), noPager)
+}
diff --git a/cmd/cli/gate/admission.go b/cmd/cli/gate/admission.go
index 0528ad9ff..bcef101a0 100644
--- a/cmd/cli/gate/admission.go
+++ b/cmd/cli/gate/admission.go
@@ -5,10 +5,11 @@ package gate
import (
"fmt"
+ "github.com/intentd/inrun/pkg/util/structured"
+
"github.com/intentd/inrun/pkg/runtime/coordinator/prepare"
"github.com/intentd/inrun/pkg/template"
"github.com/intentd/inrun/pkg/types"
- "github.com/intentd/inrun/pkg/utils"
)
// AdmissionViolation is a single fired validation rule — deny or warn.
@@ -64,9 +65,9 @@ type AdmissionMutationResult struct {
// applyMutationPreviews returns a deep copy of obj with all mutation previews
// applied. Used to simulate mutateFirst: true behaviour locally.
func applyMutationPreviews(obj map[string]interface{}, previews []AdmissionMutationPreview) map[string]interface{} {
- clone := utils.DeepCopyMap(obj)
+ clone := structured.DeepCopyMap(obj)
for _, p := range previews {
- _ = utils.SetNestedPath(clone, p.Field, p.To)
+ _ = structured.SetNestedPath(clone, p.Field, p.To)
}
return clone
}
diff --git a/cmd/cli/gate/gate.go b/cmd/cli/gate/gate.go
index 7ed82d322..eb88b1a24 100644
--- a/cmd/cli/gate/gate.go
+++ b/cmd/cli/gate/gate.go
@@ -9,6 +9,7 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
"github.com/intentd/inrun/cmd/internal"
+ "github.com/intentd/inrun/pkg/gateway/app"
"github.com/intentd/inrun/pkg/logger"
"github.com/intentd/inrun/pkg/merger"
"github.com/spf13/cobra"
@@ -22,28 +23,31 @@ func runGateway(cmd *cobra.Command, args []string) error {
paths = cmdutil.DefaultFilePaths()
}
if len(paths) == 0 {
- paths = cmdutil.Cfg.Catalog().Paths()
+ paths = cmdutil.Cfg.Blueprint().Paths()
}
if len(paths) == 0 {
- return fmt.Errorf(cmdutil.ErrNoCatalog)
+ return fmt.Errorf(cmdutil.ErrNoBlueprint)
}
+ doMigrate, _ := cmd.Flags().GetBool("migrate")
+
m := merger.New(paths...)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging catalogs: %w", err)
+ return fmt.Errorf("merging blueprints: %w", err)
}
logger.Debug().
- Str("catalogs", strings.Join(paths, ", ")).
+ Str("blueprints", strings.Join(paths, ", ")).
Int("total", m.Count()).
Int("enabled", m.EnabledCount()).
- Msg("catalogs merged")
+ Msg("blueprints merged")
- internal.RunGateway(cmdutil.Cfg, m, cmdutil.Ctx)
+ internal.RunGateway(cmdutil.Ctx, cmdutil.Cfg, m, app.GatewayOptions{Migrate: doMigrate})
return nil
}
func init() {
cmdutil.RootCmd.Args = cobra.NoArgs
cmdutil.RootCmd.RunE = runGateway
+ cmdutil.RootCmd.Flags().Bool("migrate", false, "Run schema migration before starting the gateway")
}
diff --git a/cmd/cli/gate/gate_dev.go b/cmd/cli/gate/gate_dev.go
index aa9a939d8..1c43f4a26 100644
--- a/cmd/cli/gate/gate_dev.go
+++ b/cmd/cli/gate/gate_dev.go
@@ -9,7 +9,7 @@ package gate
// process to start, but operators still need to validate admission rules before
// deploying. This command fills that gap:
//
-// inrun gate -f catalog.yaml --cr cr.yaml
+// inrun gate -f blueprint.yaml --cr cr.yaml
//
// It evaluates validation.rules and previews mutation.rules in-process, using
// the same EvaluateConditions + EvaluateValidationRule logic as the webhook and
@@ -25,93 +25,135 @@ import (
"strings"
"github.com/intentd/inrun/cmd/cli/cmdutil"
-
"github.com/intentd/inrun/cmd/internal"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
+ "github.com/intentd/inrun/pkg/gateway/app"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/template"
"github.com/intentd/inrun/pkg/types"
"github.com/spf13/cobra"
)
-var gatewayCmd = &cobra.Command{
- Use: "gate",
- Short: "Evaluate admission rules locally against a CR (no cluster required)",
- Long: `Evaluate admission rules locally against a CR.
+var gateRunCmd = &cobra.Command{
+ Use: "run",
+ Short: "Start the gateway locally (HTTP only, no TLS, no admission webhooks)",
+ Long: `Start the Inrun Gateway in local HTTP mode.
-Reads the validation.rules declared in the Catalog and runs them against the
-provided CR using the same evaluation logic as the admission webhook and the
-reconciler. No cluster, no TLS, no webhook server required.
+The Gateway API (POST /api/v1/apply, GET /api/v1/resources/, intake webhooks)
+runs on the health port (default :8080). Admission and conversion webhooks are
+disabled — TLS and a live cluster are required for those.
-Limitations:
- operator: unique — skipped (no live informer cache)
- external: calls — skipped (no real endpoint to call)
-Both are noted in the output.
+Use this to test serve routing, apply flows, and intake payloads without a
+Helm deployment.
Example:
- inrun gate -f catalog.yaml --cr cr.yaml`,
+ inrun gate run -f blueprint.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
- crFile, _ := cmd.Flags().GetString("cr")
- if crFile == "" {
- crFile = cmdutil.FindFile(cmdutil.FileCr, cmdutil.DirManifests)
- }
-
paths, _ := cmd.Flags().GetStringSlice("file")
if len(paths) == 0 {
paths = cmdutil.DefaultFilePaths()
}
if len(paths) == 0 {
- return fmt.Errorf(cmdutil.ErrNoCatalog)
+ return fmt.Errorf(cmdutil.ErrNoBlueprint)
}
+ migrate, _ := cmd.Flags().GetBool("migrate")
+
m := merger.New(paths...)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging Catalog: %w", err)
- }
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
- if err != nil {
- return fmt.Errorf("parsing Catalog: %w", err)
+ return fmt.Errorf("merging blueprints: %w", err)
}
- crData, err := cmdutil.ReadLocal(crFile)
- if err != nil {
- return fmt.Errorf("reading CR %q: %w", crFile, err)
- }
- crs := cmdutil.ParseMultiDocCRs(crData)
- if len(crs) == 0 {
- return fmt.Errorf("no valid CR documents found in %s", crFile)
- }
+ internal.RunGateway(cmdutil.Ctx, cmdutil.Cfg, m, app.GatewayOptions{Migrate: migrate})
+ return nil
+ },
+}
- fmt.Println()
- fmt.Printf("%s inrun gate\n", cmdutil.Bold("▶"))
- fmt.Printf(" %s %s\n", cmdutil.Gray("cr: "), cmdutil.Cyan(crFile))
- fmt.Printf(" %s %s\n", cmdutil.Gray("catalog:"), cmdutil.Cyan(strings.Join(paths, ", ")))
- fmt.Println()
-
- var anyDenied bool
- for _, name := range cat.CRDNames() {
- crdEntry, ok := cat.CRDEntry(name)
- if !ok {
- continue
- }
- in, ok := cmdutil.ResolveCRInputs(crs, crdEntry.APITypes.Kind)
- if !ok {
- continue
- }
- anyDenied = gateEvalCRD(cat, &crdEntry, in.CR.Object) || anyDenied
- }
+func init() {
+ // Wire admission evaluator as the default action for bare `inrun gate`.
+ gatewayCmd.RunE = gateEvalRunE
+ gatewayCmd.Long = `Evaluate admission rules locally against a CR.
+
+Reads the validation.rules declared in the Blueprint and runs them against the
+provided CR using the same evaluation logic as the admission webhook and the
+reconciler. No cluster, no TLS, no webhook server required.
+
+Limitations:
+ unique: true — skipped (no live informer cache)
+ external: calls — skipped (no real endpoint to call)
+Both are noted in the output.
+
+Example:
+ inrun gate -f blueprint.yaml --cr cr.yaml`
+
+ gatewayCmd.AddCommand(gateRunCmd)
+ gatewayCmd.Flags().String("cr", "", "CR file to evaluate (default: cr.yaml)")
+ gateRunCmd.Flags().StringSliceP("file", "f", nil, "Path(s) to blueprint.yaml (repeatable)")
+ gateRunCmd.Flags().Bool("migrate", false, "Run schema migration before starting the gateway")
+ cmdutil.ShadowGlobalCommandFlags(gateRunCmd)
+}
+
+func gateEvalRunE(cmd *cobra.Command, args []string) error {
+ crFile, _ := cmd.Flags().GetString("cr")
+ if crFile == "" {
+ crFile = cmdutil.FindFile(cmdutil.FileCr)
+ }
+
+ paths, _ := cmd.Flags().GetStringSlice("file")
+ if len(paths) == 0 {
+ paths = cmdutil.DefaultFilePaths()
+ }
+ if len(paths) == 0 {
+ return fmt.Errorf(cmdutil.ErrNoBlueprint)
+ }
+
+ m := merger.New(paths...)
+ if err := m.Merge(); err != nil {
+ return fmt.Errorf("merging Blueprint: %w", err)
+ }
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
+ if err != nil {
+ return fmt.Errorf("parsing Blueprint: %w", err)
+ }
+
+ crData, err := cmdutil.ReadLocal(crFile)
+ if err != nil {
+ return fmt.Errorf("reading CR %q: %w", crFile, err)
+ }
+ crs := cmdutil.ParseMultiDocCRs(crData)
+ if len(crs) == 0 {
+ return fmt.Errorf("no valid CR documents found in %s", crFile)
+ }
- if anyDenied {
- return fmt.Errorf("admission denied")
+ fmt.Println()
+ fmt.Printf("%s inrun gate\n", cmdutil.Bold("▶"))
+ fmt.Printf(" %s %s\n", cmdutil.Gray("cr: "), cmdutil.Cyan(crFile))
+ fmt.Printf(" %s %s\n", cmdutil.Gray("blueprint:"), cmdutil.Cyan(strings.Join(paths, ", ")))
+ fmt.Println()
+
+ var anyDenied bool
+ for _, name := range bp.CRDNames() {
+ crdEntry, ok := bp.CRDEntry(name)
+ if !ok {
+ continue
}
- return nil
- },
+ in, ok := cmdutil.ResolveCRInputs(crs, crdEntry.APITypes.Kind)
+ if !ok {
+ continue
+ }
+ anyDenied = gateEvalCRD(bp, &crdEntry, in.CR.Object) || anyDenied
+ }
+
+ if anyDenied {
+ return fmt.Errorf("admission denied")
+ }
+ return nil
}
// gateEvalCRD evaluates validation rules and previews mutation rules for one
// CRD+CR pair. Returns true if any deny-action validation rule fired.
-func gateEvalCRD(cat *catalog.Catalog, crd *types.CRDEntry, obj map[string]interface{}) bool {
+func gateEvalCRD(bp *blueprint.Blueprint, crd *types.CRDEntry, obj map[string]interface{}) bool {
fmt.Printf("%s %s %s\n", cmdutil.Cyan("◆"), cmdutil.Bold(crd.Kind()), cmdutil.Gray(fmt.Sprintf("(%s)", crd.ServeTarget())))
if !crd.HasValidationRules() && !crd.HasMutationRules() {
@@ -120,17 +162,17 @@ func gateEvalCRD(cat *catalog.Catalog, crd *types.CRDEntry, obj map[string]inter
}
// Limitation notes — printed once for the CRD.
- hasUnique := hasUniqueRule(crd)
+ hasUnique := crd.EffectiveValidation().HasUniqueRule()
hasValidationExternal := crd.EffectiveValidation() != nil && len(crd.EffectiveValidation().AdmissionExternal()) > 0
hasMutationExternal := crd.EffectiveMutation() != nil && len(crd.EffectiveMutation().AdmissionExternal()) > 0
if hasUnique {
- fmt.Printf(" %s operator: unique — skipped (no live cluster)\n", cmdutil.Dim("note:"))
+ fmt.Printf(" %s unique: true — skipped (no live cluster)\n", cmdutil.Dim("note:"))
}
if hasValidationExternal || hasMutationExternal {
fmt.Printf(" %s external: calls — skipped (no endpoint)\n", cmdutil.Dim("note:"))
}
- resolver := template.NewResolverFromMap(obj).WithUserNotes(cat.Notes)
+ resolver := template.NewResolverFromMap(obj).WithUserFunctions(bp.Functions)
eval := resolver.TemplateEvaluator()
// When mutateFirst is set the real webhook applies mutations before
@@ -141,7 +183,7 @@ func gateEvalCRD(cat *catalog.Catalog, crd *types.CRDEntry, obj map[string]inter
mutResult = EvalAdmissionMutation(obj, crd, resolver, eval)
if len(mutResult.Previews) > 0 {
validationObj = applyMutationPreviews(obj, mutResult.Previews)
- validationResolver := template.NewResolverFromMap(validationObj).WithUserNotes(cat.Notes)
+ validationResolver := template.NewResolverFromMap(validationObj).WithUserFunctions(bp.Functions)
resolver = validationResolver
eval = resolver.TemplateEvaluator()
}
@@ -223,59 +265,3 @@ func printGateMutateResult(r AdmissionMutationResult) {
fmt.Printf(" %s no mutations apply\n", cmdutil.Dim("note:"))
}
}
-
-func hasUniqueRule(crd *types.CRDEntry) bool {
- v := crd.EffectiveValidation()
- if v == nil {
- return false
- }
- for _, r := range v.Rules {
- if r.Operator == types.ConditionUnique {
- return true
- }
- }
- return false
-}
-
-var gateRunCmd = &cobra.Command{
- Use: "run",
- Short: "Start the gateway locally (HTTP only, no TLS, no admission webhooks)",
- Long: `Start the Inrun Gateway in local HTTP mode.
-
-The Gateway API (POST /api/v1/apply, GET /api/v1/resources/, intake webhooks)
-runs on the health port (default :8080). Admission and conversion webhooks are
-disabled — TLS and a live cluster are required for those.
-
-Use this to test serve routing, apply flows, and intake payloads without a
-Helm deployment.
-
-Example:
- inrun gate run -f catalog.yaml`,
- RunE: func(cmd *cobra.Command, args []string) error {
- paths, _ := cmd.Flags().GetStringSlice("file")
- if len(paths) == 0 {
- paths = cmdutil.DefaultFilePaths()
- }
- if len(paths) == 0 {
- return fmt.Errorf(cmdutil.ErrNoCatalog)
- }
-
- m := merger.New(paths...)
- if err := m.Merge(); err != nil {
- return fmt.Errorf("merging catalogs: %w", err)
- }
-
- internal.RunGatewayDev(cmdutil.Cfg, m, cmdutil.Ctx)
- return nil
- },
-}
-
-func init() {
- cmdutil.RootCmd.AddCommand(gatewayCmd)
- gatewayCmd.AddCommand(gateRunCmd)
- gatewayCmd.Flags().StringSliceP("file", "f", nil, "Path(s) to catalog.yaml (repeatable)")
- gatewayCmd.Flags().String("cr", "", "CR file to evaluate (default: cr.yaml)")
- gateRunCmd.Flags().StringSliceP("file", "f", nil, "Path(s) to catalog.yaml (repeatable)")
- cmdutil.ShadowGlobalCommandFlags(gatewayCmd)
- cmdutil.ShadowGlobalCommandFlags(gateRunCmd)
-}
diff --git a/cmd/cli/gate/gate_parent.go b/cmd/cli/gate/gate_parent.go
new file mode 100644
index 000000000..44b6f654b
--- /dev/null
+++ b/cmd/cli/gate/gate_parent.go
@@ -0,0 +1,21 @@
+//go:build !runtime
+
+package gate
+
+import (
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+ "github.com/spf13/cobra"
+)
+
+// gatewayCmd is the "gate" parent command. It is available in both dev and
+// gateway builds (//go:build !runtime) so that subcommands like "migrate"
+// work in both environments.
+var gatewayCmd = &cobra.Command{
+ Use: "gate",
+ Short: "Gateway commands — admission evaluation, local dev server, schema migration",
+}
+
+func init() {
+ cmdutil.RootCmd.AddCommand(gatewayCmd)
+ gatewayCmd.Flags().StringSliceP("file", "f", nil, "Path(s) to blueprint.yaml (repeatable)")
+}
diff --git a/cmd/cli/generate/generate.go b/cmd/cli/generate/generate.go
index a0b050228..0dcc39573 100644
--- a/cmd/cli/generate/generate.go
+++ b/cmd/cli/generate/generate.go
@@ -12,8 +12,8 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/tools/generate"
"github.com/spf13/cobra"
@@ -71,12 +71,12 @@ var generateCmd = &cobra.Command{
Short: "Generate Inrun components",
}
-// buildCatalogFromPath builds an expanded Catalog from a file path without
+// buildBlueprintFromPath builds an expanded Blueprint from a file path without
// requiring a cobra command context. Useful when the path is already known.
-func buildCatalogFromPath(path string) (*catalog.Catalog, error) {
+func buildBlueprintFromPath(path string) (*blueprint.Blueprint, error) {
m := merger.New(path)
if err := m.Merge(); err != nil {
- return nil, fmt.Errorf("merging catalog: %w", err)
+ return nil, fmt.Errorf("merging blueprint: %w", err)
}
return pipeline.BuildExpanded(cmdutil.Cfg, m)
}
@@ -84,7 +84,7 @@ func buildCatalogFromPath(path string) (*catalog.Catalog, error) {
var generateRbacCmd = &cobra.Command{
Use: "rbac",
Short: "Generate RBAC ClusterRoles and ServiceAccounts for Inrun components",
- Long: `Reads one or more catalog.yaml files, merges them, and generates minimal
+ Long: `Reads one or more blueprint.yaml files, merges them, and generates minimal
ClusterRoles for the runtime and gateway processes, plus ServiceAccounts for
all three components (runtime, gateway, console).
@@ -92,21 +92,21 @@ Use --for to limit the output to specific components. By default all three
are included. Multiple values are comma-separated.
Examples:
- inrun generate rbac -f catalog.yaml
- inrun generate rbac -f catalog.yaml --for gateway
- inrun generate rbac -f catalog.yaml --for runtime,console
+ inrun generate rbac -f blueprint.yaml
+ inrun generate rbac -f blueprint.yaml --for gateway
+ inrun generate rbac -f blueprint.yaml --for runtime,console
inrun generate rbac -f a.yaml,b.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
- out, err := cmdutil.GenerateCatalog(cmd)
+ out, err := cmdutil.GenerateBlueprint(cmd)
if err != nil {
return err
}
namespace, _ := cmd.Flags().GetString("namespace")
outputFile, _ := cmd.Flags().GetString("output")
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, out.Merger)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, out.Merger)
if err != nil {
- return fmt.Errorf("build catalog: %w", err)
+ return fmt.Errorf("build blueprint: %w", err)
}
log.Println("generating rbac...")
@@ -116,11 +116,11 @@ Examples:
return err
}
- if !cat.IsGatewayEnabled() {
+ if !bp.IsGatewayEnabled() {
opts.IncludeGateway = false
}
- runtimeRules := cat.GenerateRuntimeRBACRules()
- gatewayRules := cat.GenerateGatewayRBACRules()
+ runtimeRules := bp.GenerateRuntimeRBACRules()
+ gatewayRules := bp.GenerateGatewayRBACRules()
output, err := generate.RBACWithOptions(runtimeRules, gatewayRules, opts, namespace, outputFile)
if err != nil {
@@ -131,22 +131,22 @@ Examples:
return err
}
- return writeClusterRBACFiles(cat, outputFile)
+ return writeClusterRBACFiles(bp, outputFile)
},
}
var generateConfigMapCmd = &cobra.Command{
Use: "configmap",
- Short: "Generate a ConfigMap embedding a Catalog or Stack",
- Long: `Reads a catalog.yaml or stack.yaml file and produces a ConfigMap
-that embeds the file under data:. Useful for injecting Catalogs
+ Short: "Generate a ConfigMap embedding a Blueprint or Stack",
+ Long: `Reads a blueprint.yaml or stack.yaml file and produces a ConfigMap
+that embeds the file under data:. Useful for injecting Blueprints
into the in-cluster Inrun runtime.
Example:
- inrun generate configmap -f catalog.yaml
+ inrun generate configmap -f blueprint.yaml
inrun generate configmap -f stack.yaml -n inrun-system -o out.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
- out, err := cmdutil.GenerateCatalog(cmd)
+ out, err := cmdutil.GenerateBlueprint(cmd)
if err != nil {
return err
}
@@ -154,16 +154,16 @@ Example:
namespace, _ := cmd.Flags().GetString("namespace")
outputFile, _ := cmd.Flags().GetString("output")
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, out.Merger)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, out.Merger)
if err != nil {
- return fmt.Errorf("build catalog: %w", err)
+ return fmt.Errorf("build blueprint: %w", err)
}
log.Println("generating configmap...")
- expanded, err := cat.SerializeExpanded()
+ expanded, err := bp.SerializeExpanded()
if err != nil {
- return fmt.Errorf("serialize catalog: %w", err)
+ return fmt.Errorf("serialize blueprint: %w", err)
}
cm, err := generate.ConfigMap(expanded, namespace)
@@ -182,18 +182,18 @@ var generateBundleCmd = &cobra.Command{
• Namespace (default: 'inrun-system')
• ServiceAccounts for runtime, gateway, and console
• ClusterRoles and ClusterRoleBindings (one per process, minimal permissions)
- • ConfigMap embedding your Catalog
+ • ConfigMap embedding your Blueprint
Use --for to limit the output to specific components. By default all three
are included. Multiple values are comma-separated.
Examples:
- inrun generate bundle -f catalog.yaml
- inrun generate bundle -f catalog.yaml --for gateway
- inrun generate bundle -f catalog.yaml --for runtime,console
- inrun generate bundle -f catalog.yaml -o bundle.yaml -n inrun-system`,
+ inrun generate bundle -f blueprint.yaml
+ inrun generate bundle -f blueprint.yaml --for gateway
+ inrun generate bundle -f blueprint.yaml --for runtime,console
+ inrun generate bundle -f blueprint.yaml -o bundle.yaml -n inrun-system`,
RunE: func(cmd *cobra.Command, args []string) error {
- out, err := cmdutil.GenerateCatalog(cmd)
+ out, err := cmdutil.GenerateBlueprint(cmd)
if err != nil {
return err
}
@@ -202,9 +202,9 @@ Examples:
workloadNamespace, _ := cmd.Flags().GetString("workload-namespace")
outputFile, _ := cmd.Flags().GetString("output")
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, out.Merger)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, out.Merger)
if err != nil {
- return fmt.Errorf("build catalog: %w", err)
+ return fmt.Errorf("build blueprint: %w", err)
}
opts, err := bundleOptsFromFor(cmd)
@@ -214,15 +214,15 @@ Examples:
log.Println("generating bundle...")
- if !cat.IsGatewayEnabled() {
+ if !bp.IsGatewayEnabled() {
opts.IncludeGateway = false
}
- runtimeRules := cat.GenerateRuntimeRBACRules()
- gatewayRules := cat.GenerateGatewayRBACRules()
+ runtimeRules := bp.GenerateRuntimeRBACRules()
+ gatewayRules := bp.GenerateGatewayRBACRules()
- expanded, err := cat.SerializeExpanded()
+ expanded, err := bp.SerializeExpanded()
if err != nil {
- return fmt.Errorf("serialize catalog: %w", err)
+ return fmt.Errorf("serialize blueprint: %w", err)
}
bundle, err := generate.RenderBundle(runtimeRules, gatewayRules, expanded, namespace, workloadNamespace, opts)
@@ -234,7 +234,7 @@ Examples:
return err
}
- return writeClusterRBACFiles(cat, outputFile)
+ return writeClusterRBACFiles(bp, outputFile)
},
}
@@ -242,8 +242,8 @@ Examples:
// that has serve-enabled CRDs routed to it. Files land in the same directory as
// outputFile (or the current directory when outputFile is empty or "-").
// Template-routed CRDs appear in every cluster file; a warning is printed for those.
-func writeClusterRBACFiles(cat *catalog.Catalog, outputFile string) error {
- clusterRules, templateKinds := cat.GenerateGatewayClusterRBACRules()
+func writeClusterRBACFiles(bp *blueprint.Blueprint, outputFile string) error {
+ clusterRules, templateKinds := bp.GenerateGatewayClusterRBACRules()
if len(clusterRules) == 0 {
return nil
}
@@ -252,7 +252,7 @@ func writeClusterRBACFiles(cat *catalog.Catalog, outputFile string) error {
}
dir := clusterOutputDir(outputFile)
- clusters := cat.GatewayClusters()
+ clusters := bp.GatewayClusters()
if len(templateKinds) > 0 {
crdTxt := "CRDs"
@@ -307,9 +307,9 @@ func init() {
generateCmd.AddCommand(generateConfigMapCmd)
generateCmd.AddCommand(generateBundleCmd)
- // All three commands use StringSliceP so generateCatalog can read them uniformly.
+ // All three commands use StringSliceP so generateBlueprint can read them uniformly.
for _, cmd := range []*cobra.Command{generateConfigMapCmd, generateBundleCmd, generateRbacCmd} {
- cmd.Flags().StringSliceP("file", "f", []string{}, "Path to catalog.yaml or stack.yaml (repeatable or comma-separated)")
+ cmd.Flags().StringSliceP("file", "f", []string{}, "Path to blueprint.yaml or stack.yaml (repeatable or comma-separated)")
}
generateRegistryCmd.Flags().StringP("dirs", "d", "", "Comma-separated list of project directories to generate registries for")
@@ -342,5 +342,5 @@ func init() {
// Shadow global flags so they don't appear under `inrun generate`
cmdutil.ShadowGlobalCommandFlags(generateCmd)
- cobra.MarkFlagRequired(generateCmd.Flags(), "catalog")
+ cobra.MarkFlagRequired(generateCmd.Flags(), "blueprint")
}
diff --git a/cmd/cli/generate/helper.go b/cmd/cli/generate/helper.go
index 58432ee41..32f584932 100644
--- a/cmd/cli/generate/helper.go
+++ b/cmd/cli/generate/helper.go
@@ -98,7 +98,7 @@ func goGetModules(mods []string, timeoutPerModule time.Duration, dryRun bool) er
return nil
}
- fmt.Printf("best-effort fetching %d module(s) referenced by catalog", len(mods))
+ fmt.Printf("best-effort fetching %d module(s) referenced by blueprint", len(mods))
// ensure 'go' exists
if _, err := exec.LookPath("go"); err != nil {
diff --git a/cmd/cli/generate/registry.go b/cmd/cli/generate/registry.go
index 853a46978..3d5a3ce74 100644
--- a/cmd/cli/generate/registry.go
+++ b/cmd/cli/generate/registry.go
@@ -19,7 +19,7 @@ import (
// Generate 'pkg/typeregistry/zz_generated_typeregistry.go' for one or more operator projects.
//
-// This command scans a Catalog, resolves all CRDs, typed extensions, and
+// This command scans a Blueprint, resolves all CRDs, typed extensions, and
// managed‑resource contracts, and produces a complete runtime registry:
//
// - CRD registrations
@@ -43,30 +43,30 @@ import (
// Examples:
//
// # Generate registry for the current project
-// inrun generate registry --file catalog.yaml
+// inrun generate registry --file blueprint.yaml
//
-// # Generate registry for a specific catalog file
-// inrun generate registry --file path/to/catalog.yaml
+// # Generate registry for a specific blueprint file
+// inrun generate registry --file path/to/blueprint.yaml
//
// # Generate registry for multiple operator projects
// inrun generate registry --dirs ./website,./database,./pipeline
//
// # Dry‑run (print generated files without writing them)
-// inrun generate registry --file catalog.yaml --dry-run
+// inrun generate registry --file blueprint.yaml --dry-run
//
// Each project directory must contain:
// - go.mod
-// - a catalog.yaml (or --file pointing to one)
+// - a blueprint.yaml (or --file pointing to one)
// - pkg/typeregistry/ (created automatically if missing)
//
// The generated registry is deterministic and reflects the exact typed‑mode
-// contracts declared in the Catalog (hooks, constructors, operatorBox).
+// contracts declared in the Blueprint (hooks, constructors, operatorBox).
var generateRegistryCmd = &cobra.Command{
Use: "registry",
- Short: "Generate 'pkg/typeregistry/zz_generated_typeregistry.go' from a Catalog",
+ Short: "Generate 'pkg/typeregistry/zz_generated_typeregistry.go' from a Blueprint",
Long: `Generate the Inrun runtime registry for one or multiple operator projects.
-This command scans a Catalog, resolves all CRDs, typed extensions, and
+This command scans a Blueprint, resolves all CRDs, typed extensions, and
managed‑resource contracts, and produces the runtime registry used by the
operator process. The registry includes:
@@ -86,20 +86,20 @@ projects in one invocation using --dirs.
Examples:
# Generate registry for the current project
- inrun generate registry --file catalog.yaml
+ inrun generate registry --file blueprint.yaml
- # Generate registry for a specific catalog file
- inrun generate registry --file path/to/catalog.yaml
+ # Generate registry for a specific blueprint file
+ inrun generate registry --file path/to/blueprint.yaml
# Generate registries for multiple operator projects
inrun generate registry --dirs ./database,./pipeline --file stack.yaml
# Dry-run (print generated files without writing them)
- inrun generate registry --file catalog.yaml --dry-run
+ inrun generate registry --file blueprint.yaml --dry-run
Each project directory must contain:
• go.mod
- • a catalog.yaml
+ • a blueprint.yaml
• pkg/typeregistry/ (created automatically if missing)`,
RunE: func(cmd *cobra.Command, args []string) error {
dryRun, _ := cmd.Flags().GetBool("dry-run")
@@ -135,10 +135,10 @@ func generateRegistryForDir(dir string, cmd *cobra.Command, perModuleTimeout tim
return fmt.Errorf("chdir %s: %w", dir, err)
}
- // Load catalog (from --file or default)
- out, err := cmdutil.GenerateCatalog(cmd)
+ // Load blueprint (from --file or default)
+ out, err := cmdutil.GenerateBlueprint(cmd)
if err != nil {
- return fmt.Errorf("loading catalog: %w", err)
+ return fmt.Errorf("loading blueprint: %w", err)
}
// Validate Go project
@@ -219,26 +219,25 @@ func ensureMainGo(root, moduleName string, dryRun bool) error {
package main
import (
- "context"
+ "context"
- "github.com/intentd/inrun/cmd/cli"
- "github.com/intentd/inrun/pkg/config"
- "github.com/intentd/inrun/pkg/logger"
- "github.com/intentd/inrun/pkg/utils"
+ "github.com/intentd/inrun/cmd/cli"
+ "github.com/intentd/inrun/pkg/config"
+ "github.com/intentd/inrun/pkg/util/term"
- %s
+ %s
)
func main() {
- cfg, err := config.Init()
- if err != nil {
- logger.Fatal().AnErr("failed to load configurations", err)
- utils.Exit(err)
- }
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- cli.Execute(cfg, ctx)
-}`, timestamp, importLine)
+ cfg, err := config.Init()
+ if err != nil {
+ term.FatalExit("failed to load configurations", err)
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ cli.Execute(cfg, ctx)
+}
+`, timestamp, importLine)
if dryRun {
fmt.Println(content)
diff --git a/cmd/cli/migrate/clean.go b/cmd/cli/migrate/clean.go
new file mode 100644
index 000000000..1ef58d25d
--- /dev/null
+++ b/cmd/cli/migrate/clean.go
@@ -0,0 +1,35 @@
+//go:build !runtime
+
+package migrate
+
+import (
+ "github.com/intentd/inrun/pkg/migrate"
+ "github.com/spf13/cobra"
+)
+
+var cleanCmd = &cobra.Command{
+ Use: "clean",
+ Short: "Remove migration annotations without touching the spec",
+ Long: `Remove the migration snapshot (inrun.dev/migrate-field-snapshot or
+inrun.dev/migrate-kind-snapshot) and inrun.dev/migrate-timestamp annotations
+from CRs that carry them. The spec is left untouched.
+
+Run this after a migration is confirmed stable to discard the rollback
+safety net and release the annotation space. It is a one-way operation:
+once the snapshot is removed, rollback is no longer possible for that CR.
+
+CRs without migration annotations are skipped. Clean is idempotent.
+Requires a live cluster connection.
+
+Example:
+ inrun migrate clean -f blueprint.yaml
+ inrun migrate clean -f blueprint.yaml --dry-run
+ inrun migrate clean -f blueprint.yaml --namespace platform-01 --name my-app`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runMigrateCmd(cmd, migrate.ModeClean)
+ },
+}
+
+func init() {
+ migrateCmd.AddCommand(cleanCmd)
+}
diff --git a/cmd/cli/migrate/helper.go b/cmd/cli/migrate/helper.go
new file mode 100644
index 000000000..a457966d6
--- /dev/null
+++ b/cmd/cli/migrate/helper.go
@@ -0,0 +1,177 @@
+//go:build !runtime
+
+package migrate
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+ "github.com/intentd/inrun/cmd/cli/migrate/output"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
+ "github.com/intentd/inrun/pkg/kubeclient"
+ "github.com/intentd/inrun/pkg/labels"
+ "github.com/intentd/inrun/pkg/logger"
+ "github.com/intentd/inrun/pkg/migrate"
+ "github.com/intentd/inrun/pkg/types"
+ "github.com/spf13/cobra"
+)
+
+// runMigrateCmd runs a migrate-family operation end to end: load the blueprint,
+// build a migrator for the given mode, build the output options, and render
+// the result.
+func runMigrateCmd(cmd *cobra.Command, mode migrate.MigrateMode) error {
+ paths, err := resolvePaths(cmd)
+ if err != nil {
+ return err
+ }
+
+ bp, kube, err := pipeline.Bootstrap(cmd.Context(), cmdutil.Cfg, paths)
+ if err != nil {
+ return err
+ }
+
+ migrator, migrateOpts, err := newMigrator(cmd, kube, bp.UserFunctions(), mode)
+ if err != nil {
+ return err
+ }
+ prog := newProgress(cmd, mode)
+ defer prog.stop()
+ migrator.Progress = prog.callback()
+ migrateOpts.Progress = migrator.Progress
+
+ if mode == migrate.ModeForward && !migrateOpts.DryRun {
+ if yes, _ := cmd.Flags().GetBool("yes"); !yes {
+ ok, err := confirmPrune(cmd.Context(), bp, kube, bp.UserFunctions(), migrateOpts, prog)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ fmt.Fprintln(os.Stderr, "Aborted.")
+ return nil
+ }
+ }
+ }
+
+ opts := outputOptsFrom(cmd, paths, migrateOpts)
+ opts.Clean = mode == migrate.ModeClean
+
+ printFn, err := resolveMigrateOutputFormat(cmd, mode.String())
+ if err != nil {
+ return err
+ }
+
+ prog.begin()
+ return runMigratorForKinds(cmd.Context(), bp, migrator, opts, printFn, prog)
+}
+
+// newMigrator builds a CLI migrator for the given mode. The CLI renders its
+// own output, so the migrator logs nothing.
+func newMigrator(cmd *cobra.Command, kube kubeclient.Interface, funcs types.KELFunctionRegistry, mode migrate.MigrateMode) (*migrate.Migrator, migrate.MigratorOpts, error) {
+ dryRun, _ := cmd.Flags().GetBool("dry-run")
+ strict, _ := cmd.Flags().GetBool("strict")
+ pageSize, _ := cmd.Flags().GetInt64("page-size")
+ kind, _ := cmd.Flags().GetString("kind")
+ name, _ := cmd.Flags().GetString("name")
+ selector, _ := cmd.Flags().GetString("selector")
+ namespaces, _ := cmd.Flags().GetStringSlice("namespace")
+ force, _ := cmd.Flags().GetBool("force")
+ if _, err := labels.ValidateLabelSelector(selector); err != nil {
+ return nil, migrate.MigratorOpts{}, err
+ }
+
+ opts := migrate.MigratorOpts{
+ DryRun: dryRun,
+ Strict: strict,
+ PageSize: pageSize,
+ Kind: kind,
+ Name: name,
+ Selector: selector,
+ Namespaces: namespaces,
+ Force: force,
+ Log: logger.Nop(),
+ }
+ return migrate.New(cmd.Context(), kube, funcs, mode, opts), opts, nil
+}
+
+// outputOptsFrom builds the rendering options for a migrate-family command
+// from its flags and the already-resolved blueprint paths. It reads the same
+// flags the migrator does — dry-run, verbose, limit, kind, name, selector,
+// namespace — plus the paths resolved from --file.
+func outputOptsFrom(cmd *cobra.Command, paths []string, migrator migrate.MigratorOpts) output.Opts {
+ kind, _ := cmd.Flags().GetString("kind")
+ verbose, _ := cmd.Flags().GetBool("verbose")
+ limit, _ := cmd.Flags().GetInt("limit")
+
+ return output.Opts{
+ Kind: kind,
+ DryRun: migrator.DryRun,
+ Namespaces: migrator.Namespaces,
+ Name: migrator.Name,
+ Selector: migrator.Selector,
+ Verbose: verbose,
+ Limit: limit,
+ Paths: paths,
+ }
+}
+
+// resolvePaths reads blueprint file paths from the --file flag, falling back to
+// defaults. Returns an error if no paths are found.
+func resolvePaths(cmd *cobra.Command) ([]string, error) {
+ paths, _ := cmd.Flags().GetStringSlice("file")
+ if len(paths) == 0 {
+ paths = cmdutil.DefaultFilePaths()
+ }
+ if len(paths) == 0 {
+ return nil, fmt.Errorf(cmdutil.ErrNoBlueprint)
+ }
+ return paths, nil
+}
+
+// runMigratorForKinds runs the migrator over bp and prints the results, also
+// when the run fails part way.
+func runMigratorForKinds(ctx context.Context, bp *blueprint.Blueprint, migrator *migrate.Migrator, opts output.Opts, printFn func(output.Opts), prog *progress) error {
+ results, err := migrator.Run(ctx, bp)
+ prog.stop()
+ opts.Results = results
+ printFn(opts)
+ return err
+}
+
+// resolveMigrateOutputFormat maps the --output flag to the printFn for a
+// migrate or rollback run: "text" (default) for the human printer, "json"
+// for the machine-readable one. mode selects which pair of printers to use.
+func resolveMigrateOutputFormat(cmd *cobra.Command, mode string) (func(output.Opts), error) {
+ outputFormat, _ := cmd.Flags().GetString("output")
+ switch outputFormat {
+ case "text", "":
+ return textPrinterFor(mode), nil
+ case "json":
+ return jsonPrinterFor(mode), nil
+ default:
+ return nil, fmt.Errorf("unknown output format %q (want text or json)", outputFormat)
+ }
+}
+
+// textPrinterFor returns the human printer for the given mode.
+// mode is "migrate" or "rollback" — the same string the CLI's subcommand uses.
+func textPrinterFor(mode string) func(output.Opts) {
+ switch mode {
+ case "rollback":
+ return output.PrintRollback
+ default:
+ return output.PrintMigrate
+ }
+}
+
+// jsonPrinterFor returns the JSON printer for the given mode.
+func jsonPrinterFor(mode string) func(output.Opts) {
+ switch mode {
+ case "rollback":
+ return output.PrintRollbackJSON
+ default:
+ return output.PrintMigrateJSON
+ }
+}
diff --git a/cmd/cli/migrate/migrate.go b/cmd/cli/migrate/migrate.go
index 5182d7c5d..6842ebfac 100644
--- a/cmd/cli/migrate/migrate.go
+++ b/cmd/cli/migrate/migrate.go
@@ -1,193 +1,61 @@
-//go:build !runtime && !gateway
+//go:build !runtime
-// Package migrate implements the migrate command, which converts a
-// controller-runtime reconciler to run on this runtime.
package migrate
import (
- "bufio"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
"github.com/intentd/inrun/cmd/cli/cmdutil"
-
- "github.com/intentd/inrun/pkg/tools/migrate"
- "github.com/intentd/inrun/pkg/version"
+ "github.com/intentd/inrun/pkg/migrate"
"github.com/spf13/cobra"
)
var migrateCmd = &cobra.Command{
- Use: "migrate ",
- Short: "Migrate a controller-runtime operator to Inrun",
- Long: `Migrates a controller-runtime reconciler to Inrun. Your Reconcile logic
-is untouched — Inrun takes over the infrastructure.
-
-Default mode (--mode toclient): zero changes to your Reconcile signature or
-call sites. SetupWithManager is removed; a two-line constructor using
-adapter.ToClient and domain.ReconcilerFrom is injected. Your reconciler
-compiles and runs inside Inrun with no other edits.
-
- inrun migrate ./controller/webapp_controller.go -o ./my-operator
-
-The output directory receives the rewritten file plus scaffolding:
-catalog.yaml, simulate.yaml, e2e.yaml, go.mod, Makefile, Dockerfile.
-
-For a full rewrite to idiomatic Inrun style (new Reconcile signature,
-struct fields, call sites), use --mode native.
-
-Examples:
- inrun migrate ./controller/webapp_controller.go -o ./my-operator
- inrun migrate ./controller/webapp_controller.go --mode native -o ./out
- inrun migrate ./controller/webapp_controller.go # prompts before replacing`,
- Args: cobra.ExactArgs(1),
+ Use: "migrate",
+ Short: "Migrate CR spec fields declared with migrate.from in the Blueprint",
+ Long: `Apply schema migrations to live CRs using migrate.from declarations in the Blueprint.
+
+For each serve field with migrate.from set, the migrator:
+ 1. reads the old value from the migrate.from spec path
+ 2. applies the field's value/values transform (if any)
+ 3. writes the result to the field's current spec path
+ 4. removes the migrate.from path
+
+Migration is idempotent — CRs where migrate.from is absent are skipped.
+Requires a live cluster connection.
+
+Subcommands:
+ rollback Restore CRs from their migration snapshots
+ clean Remove migration annotations without touching the spec
+
+Example:
+ inrun migrate -f blueprint.yaml
+ inrun migrate -f blueprint.yaml --dry-run
+ inrun migrate -f blueprint.yaml --kind WebApp`,
RunE: func(cmd *cobra.Command, args []string) error {
- inputPath := args[0]
- outputDir, _ := cmd.Flags().GetString("output")
- modulePath, _ := cmd.Flags().GetString("module")
- operatorName, _ := cmd.Flags().GetString("name")
- modeFlag, _ := cmd.Flags().GetString("mode")
-
- var mode migrate.Mode
- switch modeFlag {
- case "native":
- mode = migrate.ModeNative
- case "toclient", "":
- mode = migrate.ModeToClient
- default:
- return fmt.Errorf("unknown --mode %q: valid values are toclient, native", modeFlag)
- }
-
- src, err := cmdutil.ReadLocal(inputPath)
- if err != nil {
- return fmt.Errorf("read %s: %w", inputPath, err)
- }
-
- res, err := migrate.Rewrite(src, mode)
- if err != nil {
- return fmt.Errorf("migrate: %w", err)
- }
-
- if len(res.Warnings) > 0 {
- fmt.Fprintf(os.Stderr, "\n%s\n", cmdutil.Yellow("Warnings — review these before running:"))
- for _, w := range res.Warnings {
- fmt.Fprintf(os.Stderr, " %s %s\n", cmdutil.Yellow("⚠"), w)
- }
- fmt.Fprintln(os.Stderr)
- }
-
- opts := migrate.Options{
- ModulePath: modulePath,
- OperatorName: operatorName,
- InrunVersion: version.Short(),
- }
- files := migrate.Generate(res, opts)
-
- if outputDir != "" {
- return writeOutputDir(outputDir, inputPath, res, files)
- }
-
- return replaceInPlace(inputPath, res, files)
+ return runMigrateCmd(cmd, migrate.ModeForward)
},
}
-func writeOutputDir(dir, inputPath string, res *migrate.Result, files migrate.Files) error {
- if err := os.MkdirAll(dir, 0o755); err != nil {
- return fmt.Errorf("create output dir: %w", err)
- }
-
- reconcilerDest := filepath.Join(dir, filepath.Base(inputPath))
- if err := os.WriteFile(reconcilerDest, res.Source, 0o644); err != nil {
- return fmt.Errorf("write reconciler: %w", err)
- }
- fmt.Printf(" %s %s\n", cmdutil.Green("→"), reconcilerDest)
-
- type namedFile struct {
- name string
- content string
- }
- generated := []namedFile{
- {"catalog.yaml", files.Catalog},
- {"test/simulate.yaml", files.Simulate},
- {"test/e2e.yaml", files.E2E},
- {"go.mod", files.GoMod},
- {"Makefile", files.Makefile},
- {"Dockerfile", files.Dockerfile},
- {"README.md", files.README},
- }
- for _, f := range generated {
- dest := filepath.Join(dir, f.name)
- if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
- return fmt.Errorf("create %s: %w", filepath.Dir(dest), err)
- }
- if err := os.WriteFile(dest, []byte(f.content), 0o644); err != nil {
- return fmt.Errorf("write %s: %w", f.name, err)
- }
- fmt.Printf(" %s %s\n", cmdutil.Green("→"), dest)
- }
-
- fmt.Printf("\n%s Search for %s in %s to see what needs attention.\n",
- cmdutil.Green("✓ Done."), cmdutil.Bold("TODO(inrun migrate)"), dir)
- return nil
-}
-
-func replaceInPlace(inputPath string, res *migrate.Result, files migrate.Files) error {
- dir := filepath.Dir(inputPath)
-
- fmt.Printf("This will replace %s and write %s alongside it.\n",
- cmdutil.Bold(inputPath), cmdutil.Bold("catalog.yaml, test/simulate.yaml, test/e2e.yaml, go.mod, Makefile, Dockerfile"))
- fmt.Printf("%s [y/N] ", cmdutil.Yellow("Continue?"))
-
- scanner := bufio.NewScanner(os.Stdin)
- scanner.Scan()
- answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
- if answer != "y" && answer != "yes" {
- fmt.Println("Aborted.")
- return nil
- }
-
- if err := os.WriteFile(inputPath, res.Source, 0o644); err != nil {
- return fmt.Errorf("write %s: %w", inputPath, err)
- }
- fmt.Printf(" %s %s\n", cmdutil.Green("→"), inputPath)
-
- type namedFile struct {
- name string
- content string
- }
- generated := []namedFile{
- {"catalog.yaml", files.Catalog},
- {"test/simulate.yaml", files.Simulate},
- {"test/e2e.yaml", files.E2E},
- {"go.mod", files.GoMod},
- {"Makefile", files.Makefile},
- {"Dockerfile", files.Dockerfile},
- }
- for _, f := range generated {
- dest := filepath.Join(dir, f.name)
- if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
- return fmt.Errorf("create %s: %w", filepath.Dir(dest), err)
- }
- if err := os.WriteFile(dest, []byte(f.content), 0o644); err != nil {
- return fmt.Errorf("write %s: %w", f.name, err)
- }
- fmt.Printf(" %s %s\n", cmdutil.Green("→"), dest)
- }
-
- fmt.Printf("\n%s Search for %s to see what needs attention.\n",
- cmdutil.Green("✓ Done."), cmdutil.Bold("TODO(inrun migrate)"))
- return nil
-}
-
func init() {
cmdutil.RootCmd.AddCommand(migrateCmd)
- migrateCmd.Flags().StringP("output", "o", "", "Write output to this directory (non-destructive; skips confirmation)")
- migrateCmd.Flags().String("module", "", "Go module path for the migrated operator (e.g. github.com/myorg/my-operator)")
- migrateCmd.Flags().String("name", "", "Operator name in kebab-case (e.g. my-operator); derived from receiver type if omitted")
- migrateCmd.Flags().String("mode", "toclient", "Migration mode: toclient (default, zero Reconcile changes) or native (full rewrite)")
-
- // Shadow global flags so they don't appear under `inrun migrate`
- cmdutil.ShadowGlobalCommandFlags(migrateCmd, "file")
+ // Persistent flags are inherited by every subcommand (rollback, clean).
+ // Declare shared options here so a new subcommand picks them up without
+ // re-registering, and so `inrun migrate --help` lists them once.
+ migrateCmd.PersistentFlags().StringSliceP("file", "f", nil, "Path(s) to blueprint.yaml (repeatable)")
+ migrateCmd.PersistentFlags().Bool("dry-run", false, "Preview changes without writing to the cluster")
+ migrateCmd.PersistentFlags().Bool("strict", false, "Fail if any individual CR cannot be migrated (default: warn and skip)")
+ migrateCmd.PersistentFlags().Bool("verbose", false, "Print the resulting spec for each migrated CR")
+ migrateCmd.PersistentFlags().Int("limit", 0, "Max CRs to show in verbose mode (0 = all)")
+ migrateCmd.PersistentFlags().Int64("page-size", 0, "CRs fetched per List call (0 = API server default, useful for testing pagination)")
+ migrateCmd.PersistentFlags().StringP("kind", "k", "", "Migrate only this kind (default: all kinds with migrate.from)")
+ migrateCmd.PersistentFlags().String("name", "", "Migrate only the CR with this name")
+ migrateCmd.PersistentFlags().String("selector", "", "Select CRs by label (Kubernetes label selector syntax)")
+ migrateCmd.PersistentFlags().StringSliceP("namespace", "n", nil, "Restrict migration to these namespaces (comma-separated; default: all)")
+ migrateCmd.PersistentFlags().StringP("output", "o", "text", "Output format: text, json")
+ migrateCmd.PersistentFlags().Bool("force", false, "Take ownership of fields owned by another manager when applying the crd.migrate.to destination (SSA force). Default: fail on conflict.")
+
+ migrateCmd.Flags().BoolP("yes", "y", false, "Do not ask before migrate.prune deletes source CRs")
+
+ // Shadow global flags so they don't appear under `inrun version`
+ cmdutil.ShadowGlobalCommandFlags(migrateCmd)
}
diff --git a/cmd/cli/migrate/output/json.go b/cmd/cli/migrate/output/json.go
new file mode 100644
index 000000000..9691ae1e6
--- /dev/null
+++ b/cmd/cli/migrate/output/json.go
@@ -0,0 +1,171 @@
+//go:build !runtime && !gateway
+
+package output
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/intentd/inrun/pkg/migrate"
+)
+
+// MigrateReport is the machine-readable form of a migrate, rollback, or clean
+// run. The field names are a stable interface: consumers rely on them for CI
+// checks, diffing, and audit logs. Change them only with a version bump.
+//
+// The report mirrors PrintMigrate/PrintRollback's vocabulary — "migrated",
+// "drifted", "skipped", "failed" — so a script and a human reading the same
+// run see the same buckets.
+type MigrateReport struct {
+ Mode string `json:"mode"` // "forward" | "rollback" | "clean"
+ DryRun bool `json:"dryRun"`
+ Blueprint []string `json:"blueprint"`
+ Kind string `json:"kind,omitempty"`
+ Name string `json:"name,omitempty"`
+ Selector string `json:"selector,omitempty"`
+ Namespaces []string `json:"namespaces,omitempty"`
+ Kinds []KindReport `json:"kinds"`
+ Totals ReportTotals `json:"totals"`
+}
+
+// KindReport is the per-kind section of the report.
+type KindReport struct {
+ Kind string `json:"kind"`
+ To string `json:"to,omitempty"`
+ Scope *Scope `json:"scope,omitempty"`
+ CRs []CRReport `json:"crs"`
+}
+
+// Scope is the selection a kind ran with: the blueprint's migrate.selection
+// merged with the CLI flags.
+type Scope struct {
+ Namespaces []string `json:"namespaces,omitempty"`
+ Name string `json:"name,omitempty"`
+ Selector string `json:"selector,omitempty"`
+}
+
+// CRReport is one CR's outcome. Ref is "namespace/name" — the same form the
+// human output uses. Fields, DriftedFields, and Error are omitted when empty
+// so a clean run reads as small JSON.
+type CRReport struct {
+ Ref string `json:"ref"`
+ Fields []string `json:"fields,omitempty"`
+ DriftedFields []string `json:"driftedFields,omitempty"`
+ AllDrifted bool `json:"allDrifted,omitempty"`
+ Drifted bool `json:"drifted,omitempty"`
+ Pruned bool `json:"pruned,omitempty"`
+ AfterSpec map[string]interface{} `json:"afterSpec,omitempty"`
+ Skipped bool `json:"skipped,omitempty"`
+ Excluded bool `json:"excluded,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+// ReportTotals aggregates the per-CR buckets across all kinds. A consumer can
+// fail a pipeline on Totals.Failed > 0 or Totals.Drifted > 0 without walking
+// the per-CR list.
+type ReportTotals struct {
+ CRs int `json:"crs"`
+ Migrated int `json:"migrated"`
+ Drifted int `json:"drifted"`
+ AllDrifted int `json:"allDrifted"`
+ Skipped int `json:"skipped"`
+ Excluded int `json:"excluded"`
+ Pruned int `json:"pruned"`
+ Failed int `json:"failed"`
+}
+
+// writeJSON writes the run's results as indented JSON to w. mode is one of
+// "forward", "rollback", "clean" — the same strings that appear in the report.
+func writeJSON(w io.Writer, mode string, opts Opts) error {
+ report := buildReport(mode, opts)
+ enc := json.NewEncoder(w)
+ enc.SetIndent("", " ")
+ return enc.Encode(report)
+}
+
+// PrintMigrateJSON is the printFn-shaped entry point for JSON migrate output.
+// Errors go to stderr so stdout stays a single valid JSON document.
+func PrintMigrateJSON(opts Opts) {
+ if err := writeJSON(os.Stdout, migrate.ModeForward.String(), opts); err != nil {
+ fmt.Fprintf(os.Stderr, "marshal json: %v\n", err)
+ }
+}
+
+// PrintRollbackJSON is the printFn-shaped entry point for JSON rollback output.
+func PrintRollbackJSON(opts Opts) {
+ if err := writeJSON(os.Stdout, migrate.ModeRollback.String(), opts); err != nil {
+ fmt.Fprintf(os.Stderr, "marshal json: %v\n", err)
+ }
+}
+
+// buildReport converts Opts into a MigrateReport, computing the same totals
+// the human summary line shows.
+func buildReport(mode string, opts Opts) MigrateReport {
+ report := MigrateReport{
+ Mode: mode,
+ DryRun: opts.DryRun,
+ Blueprint: opts.Paths,
+ Kind: opts.Kind,
+ Name: opts.Name,
+ Selector: opts.Selector,
+ Namespaces: opts.Namespaces,
+ }
+ for _, r := range opts.Results {
+ if r == nil {
+ continue
+ }
+ kind := KindReport{Kind: r.Kind, To: r.To}
+ if !r.Selection.IsEmpty() {
+ kind.Scope = &Scope{Namespaces: r.Selection.Namespaces, Name: r.Selection.Name, Selector: r.Selection.Selector}
+ }
+ for _, cr := range r.CRs {
+ kind.CRs = append(kind.CRs, crReport(cr))
+ report.Totals.CRs++
+ switch {
+ case cr.Err != nil:
+ report.Totals.Failed++
+ case cr.Excluded:
+ report.Totals.Excluded++
+ case cr.Skipped:
+ report.Totals.Skipped++
+ case cr.AllDrifted:
+ report.Totals.AllDrifted++
+ case cr.Drifted:
+ report.Totals.Drifted++
+ default:
+ if len(cr.Fields) > 0 {
+ report.Totals.Migrated++
+ }
+ if len(cr.DriftedFields) > 0 {
+ report.Totals.Drifted++
+ }
+ }
+ if cr.Err == nil && cr.Pruned {
+ report.Totals.Pruned++
+ }
+ }
+ report.Kinds = append(report.Kinds, kind)
+ }
+ return report
+}
+
+// crReport converts one migrate.CRMigrateResult to its JSON form. Error is stringified.
+func crReport(cr migrate.CRMigrateResult) CRReport {
+ out := CRReport{
+ Ref: cr.Ref(),
+ Fields: cr.Fields,
+ DriftedFields: cr.DriftedFields,
+ AllDrifted: cr.AllDrifted,
+ Drifted: cr.Drifted,
+ Pruned: cr.Pruned,
+ AfterSpec: cr.AfterSpec,
+ Skipped: cr.Skipped,
+ Excluded: cr.Excluded,
+ }
+ if cr.Err != nil {
+ out.Error = cr.Err.Error()
+ }
+ return out
+}
diff --git a/cmd/cli/migrate/output/migrate.go b/cmd/cli/migrate/output/migrate.go
new file mode 100644
index 000000000..491757f1b
--- /dev/null
+++ b/cmd/cli/migrate/output/migrate.go
@@ -0,0 +1,7 @@
+//go:build gateway
+
+package output
+
+// PrintMigrate is a no-op in gateway builds — migration output comes from the
+// structured logger, not a terminal-formatted printer.
+func PrintMigrate(_ Opts) {}
diff --git a/cmd/cli/migrate/output/migrate_dev.go b/cmd/cli/migrate/output/migrate_dev.go
new file mode 100644
index 000000000..6561e4a1e
--- /dev/null
+++ b/cmd/cli/migrate/output/migrate_dev.go
@@ -0,0 +1,221 @@
+//go:build !runtime && !gateway
+
+package output
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+ "github.com/intentd/inrun/pkg/migrate"
+ sigsyaml "sigs.k8s.io/yaml"
+)
+
+func PrintMigrate(opts Opts) {
+ fmt.Println()
+ op := "migrate"
+ if opts.Clean {
+ op = "migrate clean"
+ }
+ label := cmdutil.Bold("⟳") + " inrun " + op
+ if opts.DryRun {
+ label += " " + cmdutil.Yellow("[dry-run]")
+ }
+ fmt.Println(label)
+ fmt.Printf(" %s %s\n", cmdutil.Gray("blueprint:"), cmdutil.Cyan(strings.Join(opts.Paths, ", ")))
+ if len(opts.Namespaces) > 0 {
+ nsStr := "namespace"
+ if len(opts.Namespaces) > 1 {
+ nsStr += "s"
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Gray(nsStr+":"), cmdutil.Cyan(strings.Join(opts.Namespaces, ", ")))
+ }
+ if opts.Name != "" {
+ fmt.Printf(" %s %s\n", cmdutil.Gray("name:"), cmdutil.Cyan(opts.Name))
+ }
+ if opts.Selector != "" {
+ fmt.Printf(" %s %s\n", cmdutil.Gray("selector:"), cmdutil.Cyan(opts.Selector))
+ }
+ if opts.Kind != "" {
+ fmt.Printf(" %s %s\n", cmdutil.Gray("kind:"), cmdutil.Cyan(opts.Kind))
+ }
+ fmt.Println()
+
+ for _, r := range opts.Results {
+ if r == nil {
+ continue
+ }
+
+ var c migrateCounts
+ for _, cr := range r.CRs {
+ c.add(cr)
+ }
+
+ total := len(r.CRs)
+ printKindHeader(r, total, opts)
+
+ verboseShown := 0
+ for _, cr := range r.CRs {
+ ref := cr.Ref()
+ switch {
+ case cr.Err != nil:
+ fmt.Printf(" %s %s %s\n", cmdutil.FailureMark(), cmdutil.Red(ref), cmdutil.Red(cr.Err.Error()))
+ case cr.Drifted:
+ fmt.Printf(" %s %s %s\n", cmdutil.Dim("~"), cmdutil.Gray(ref), cmdutil.Yellow("destination exists and differs — skipped"))
+ case cr.Excluded:
+ fmt.Printf(" %s %s\n", cmdutil.Dim("–"), cmdutil.Gray(ref+" (not selected: migrate.selection when/or)"))
+ case cr.Skipped:
+ skippedMsg := "(already migrated)"
+ if opts.Clean {
+ skippedMsg = "(no annotations)"
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Dim("–"), cmdutil.Gray(ref+" "+skippedMsg))
+ case len(cr.Fields) == 0 && cr.Pruned:
+ fmt.Printf(" %s %s %s\n", cmdutil.SuccessMark(), cmdutil.Cyan(ref), cmdutil.Gray("already migrated; "+pruneVerb(opts.DryRun)))
+ default:
+ var verb string
+ switch {
+ case opts.Clean && opts.DryRun:
+ verb = "would clean"
+ case opts.Clean:
+ verb = "cleaned"
+ case opts.DryRun:
+ verb = "would migrate"
+ default:
+ verb = "migrated"
+ }
+ line := verb + ": " + strings.Join(cr.Fields, ", ")
+ if r.To != "" && !opts.Clean {
+ line = verb + " to " + r.To
+ }
+ if cr.Pruned {
+ line += "; " + pruneVerb(opts.DryRun)
+ }
+ fmt.Printf(" %s %s %s\n", cmdutil.SuccessMark(), cmdutil.Cyan(ref), cmdutil.Gray(line))
+ if opts.Verbose && cr.AfterSpec != nil {
+ if opts.Limit == 0 || verboseShown < opts.Limit {
+ printSpec(cr.AfterSpec)
+ verboseShown++
+ }
+ }
+ }
+ }
+
+ if opts.Verbose && opts.Limit > 0 && verboseShown >= opts.Limit && c.migrated > opts.Limit {
+ fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("… %d more (use --limit 0 to show all)", c.migrated-opts.Limit)))
+ }
+
+ fmt.Printf("\n %s\n\n", buildMigrateSummary(c, total, opts.DryRun, opts.Clean))
+ }
+}
+
+// migrateCounts buckets the CRs of one kind for the summary line.
+type migrateCounts struct {
+ migrated, skipped, excluded, drifted, pruned, failed int
+}
+
+func (c *migrateCounts) add(cr migrate.CRMigrateResult) {
+ switch {
+ case cr.Err != nil:
+ c.failed++
+ return
+ case cr.Drifted:
+ c.drifted++
+ case cr.Excluded:
+ c.excluded++
+ case cr.Skipped:
+ c.skipped++
+ case len(cr.Fields) > 0:
+ c.migrated++
+ }
+ if cr.Pruned {
+ c.pruned++
+ }
+}
+
+func pruneVerb(dryRun bool) string {
+ if dryRun {
+ return "would prune source"
+ }
+ return "source pruned"
+}
+
+// printKindHeader prints the kind line, and the scope the run used when it
+// differs from the CLI flags (the blueprint's migrate.selection applied).
+func printKindHeader(r *migrate.KindMigrateResult, total int, opts Opts) {
+ countStr := fmt.Sprintf("%d CR", total)
+ if total != 1 {
+ countStr += "s"
+ }
+ name := cmdutil.Bold(r.Kind)
+ if r.To != "" {
+ name += cmdutil.Gray(" → ") + cmdutil.Bold(r.To)
+ }
+ fmt.Printf("%s %s %s\n", cmdutil.Cyan("◆"), name, cmdutil.Gray(countStr))
+
+ flags := migrate.Selection{Namespaces: opts.Namespaces, Name: opts.Name, Selector: opts.Selector}
+ if r.Selection.IsEmpty() || r.Selection.Equal(flags) {
+ return
+ }
+ var parts []string
+ if len(r.Selection.Namespaces) > 0 {
+ parts = append(parts, "namespaces "+strings.Join(r.Selection.Namespaces, ","))
+ }
+ if r.Selection.Name != "" {
+ parts = append(parts, "name "+r.Selection.Name)
+ }
+ if r.Selection.Selector != "" {
+ parts = append(parts, "selector "+r.Selection.Selector)
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Gray("scope:"), cmdutil.Cyan(strings.Join(parts, " · ")))
+}
+
+func buildMigrateSummary(c migrateCounts, total int, dryRun, clean bool) string {
+ var verb string
+ switch {
+ case clean && dryRun:
+ verb = "would be cleaned"
+ case clean:
+ verb = "cleaned"
+ case dryRun:
+ verb = "would be migrated"
+ default:
+ verb = "migrated"
+ }
+ parts := []string{fmt.Sprintf("%d/%d %s", c.migrated, total, verb)}
+ if c.pruned > 0 {
+ pruned := "sources pruned"
+ if dryRun {
+ pruned = "sources would be pruned"
+ }
+ parts = append(parts, cmdutil.Yellow(fmt.Sprintf("%d %s", c.pruned, pruned)))
+ }
+ if c.drifted > 0 {
+ parts = append(parts, cmdutil.Yellow(fmt.Sprintf("%d drifted", c.drifted)))
+ }
+ if c.skipped > 0 {
+ parts = append(parts, cmdutil.Gray(fmt.Sprintf("%d already done", c.skipped)))
+ }
+ if c.excluded > 0 {
+ parts = append(parts, cmdutil.Gray(fmt.Sprintf("%d not selected", c.excluded)))
+ }
+ if c.failed > 0 {
+ parts = append(parts, cmdutil.Red(fmt.Sprintf("%d failed", c.failed)))
+ }
+ mark := cmdutil.SuccessMark()
+ if c.failed > 0 {
+ mark = cmdutil.FailureMark()
+ }
+ return mark + " " + strings.Join(parts, " · ")
+}
+
+func printSpec(spec map[string]interface{}) {
+ b, err := sigsyaml.Marshal(spec)
+ if err != nil {
+ return
+ }
+ fmt.Printf(" %s\n", cmdutil.Gray("spec:"))
+ for _, line := range strings.Split(strings.TrimRight(string(b), "\n"), "\n") {
+ fmt.Printf(" %s\n", cmdutil.Dim(line))
+ }
+}
diff --git a/cmd/cli/migrate/output/rollback.go b/cmd/cli/migrate/output/rollback.go
new file mode 100644
index 000000000..658af2560
--- /dev/null
+++ b/cmd/cli/migrate/output/rollback.go
@@ -0,0 +1,133 @@
+//go:build !runtime && !gateway
+
+package output
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+)
+
+func PrintRollback(opts Opts) {
+ fmt.Println()
+ label := cmdutil.Bold("⟳") + " inrun migrate rollback"
+ if opts.DryRun {
+ label += " " + cmdutil.Yellow("[dry-run]")
+ }
+ fmt.Println(label)
+ fmt.Printf(" %s %s\n", cmdutil.Gray("blueprint:"), cmdutil.Cyan(strings.Join(opts.Paths, ", ")))
+ if len(opts.Namespaces) > 0 {
+ nsStr := "namespace"
+ if len(opts.Namespaces) != 1 {
+ nsStr += "s"
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Gray(nsStr+":"), cmdutil.Cyan(strings.Join(opts.Namespaces, ", ")))
+ }
+ if opts.Name != "" {
+ fmt.Printf(" %s %s\n", cmdutil.Gray("name:"), cmdutil.Cyan(opts.Name))
+ }
+ if opts.Selector != "" {
+ fmt.Printf(" %s %s\n", cmdutil.Gray("selector:"), cmdutil.Cyan(opts.Selector))
+ }
+ if opts.Kind != "" {
+ fmt.Printf(" %s %s\n", cmdutil.Gray("kind:"), cmdutil.Cyan(opts.Kind))
+ }
+ fmt.Println()
+
+ for _, r := range opts.Results {
+ if r == nil {
+ continue
+ }
+
+ rolledBack, skipped, drifted, allDrifted, failed := 0, 0, 0, 0, 0
+ for _, cr := range r.CRs {
+ switch {
+ case cr.Err != nil:
+ failed++
+ case cr.Skipped:
+ skipped++
+ case cr.AllDrifted:
+ allDrifted++
+ default:
+ if len(cr.Fields) > 0 {
+ rolledBack++
+ }
+ if len(cr.DriftedFields) > 0 {
+ drifted++
+ }
+ }
+ }
+
+ total := len(r.CRs)
+ printKindHeader(r, total, opts)
+
+ verboseShown := 0
+ for _, cr := range r.CRs {
+ ref := cr.Ref()
+ switch {
+ case cr.Err != nil:
+ fmt.Printf(" %s %s %s\n", cmdutil.FailureMark(), cmdutil.Red(ref), cmdutil.Red(cr.Err.Error()))
+ case cr.Skipped:
+ fmt.Printf(" %s %s\n", cmdutil.Dim("–"), cmdutil.Gray(ref+" (no snapshot)"))
+ case cr.AllDrifted && r.To != "":
+ fmt.Printf(" %s %s %s\n", cmdutil.Dim("~"), cmdutil.Gray(ref), cmdutil.Yellow("destination modified after migration — left in place"))
+ default:
+ if len(cr.Fields) > 0 {
+ verb := "rolled back"
+ if opts.DryRun {
+ verb = "would roll back"
+ }
+ line := verb + ": " + strings.Join(cr.Fields, ", ")
+ if r.To != "" {
+ line = verb + " to " + r.Kind + "; " + r.To + " deleted"
+ if opts.DryRun {
+ line = verb + " to " + r.Kind + "; would delete " + r.To
+ }
+ }
+ fmt.Printf(" %s %s %s\n", cmdutil.SuccessMark(), cmdutil.Cyan(ref), cmdutil.Gray(line))
+ if opts.Verbose && cr.AfterSpec != nil {
+ if opts.Limit == 0 || verboseShown < opts.Limit {
+ printSpec(cr.AfterSpec)
+ verboseShown++
+ }
+ }
+ }
+ for _, f := range cr.DriftedFields {
+ fmt.Printf(" %s %s %s\n", cmdutil.Dim("~"), cmdutil.Gray(ref), cmdutil.Dim(f+": drifted — modified after migration, skipped"))
+ }
+ }
+ }
+
+ if opts.Verbose && opts.Limit > 0 && verboseShown >= opts.Limit && rolledBack > opts.Limit {
+ fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("… %d more (use --limit 0 to show all)", rolledBack-opts.Limit)))
+ }
+
+ fmt.Printf("\n %s\n\n", buildRollbackSummary(rolledBack, skipped, drifted, allDrifted, failed, total, opts.DryRun))
+ }
+}
+
+func buildRollbackSummary(rolledBack, skipped, drifted, allDrifted, failed, total int, dryRun bool) string {
+ verb := "rolled back"
+ if dryRun {
+ verb = "would be rolled back"
+ }
+ parts := []string{fmt.Sprintf("%d/%d %s", rolledBack, total, verb)}
+ if drifted > 0 {
+ parts = append(parts, cmdutil.Yellow(fmt.Sprintf("%d drifted", drifted)))
+ }
+ if allDrifted > 0 {
+ parts = append(parts, cmdutil.Yellow(fmt.Sprintf("%d all fields drifted", allDrifted)))
+ }
+ if skipped > 0 {
+ parts = append(parts, cmdutil.Gray(fmt.Sprintf("%d no snapshot", skipped)))
+ }
+ if failed > 0 {
+ parts = append(parts, cmdutil.Red(fmt.Sprintf("%d failed", failed)))
+ }
+ mark := cmdutil.SuccessMark()
+ if failed > 0 {
+ mark = cmdutil.FailureMark()
+ }
+ return mark + " " + strings.Join(parts, " · ")
+}
diff --git a/cmd/cli/migrate/output/type.go b/cmd/cli/migrate/output/type.go
new file mode 100644
index 000000000..5b51f503c
--- /dev/null
+++ b/cmd/cli/migrate/output/type.go
@@ -0,0 +1,19 @@
+//go:build !runtime
+
+package output
+
+import "github.com/intentd/inrun/pkg/migrate"
+
+// Opts carries the data needed by every gate output printer.
+type Opts struct {
+ Results []*migrate.KindMigrateResult
+ DryRun bool
+ Clean bool
+ Verbose bool
+ Limit int
+ Paths []string
+ Namespaces []string
+ Name string
+ Kind string
+ Selector string
+}
diff --git a/cmd/cli/migrate/progress.go b/cmd/cli/migrate/progress.go
new file mode 100644
index 000000000..89b082f56
--- /dev/null
+++ b/cmd/cli/migrate/progress.go
@@ -0,0 +1,71 @@
+//go:build !runtime
+
+package migrate
+
+import (
+ "fmt"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+ "github.com/intentd/inrun/pkg/migrate"
+ "github.com/intentd/inrun/pkg/util/term"
+ "github.com/spf13/cobra"
+)
+
+// progress shows a spinner while a run works through CRs. It is nil, and
+// every method a no-op, unless output is text to a terminal.
+type progress struct {
+ verb string
+ spin *term.Spinner
+}
+
+func newProgress(cmd *cobra.Command, mode migrate.MigrateMode) *progress {
+ if out, _ := cmd.Flags().GetString("output"); out != "text" || !term.IsTerminal() {
+ return nil
+ }
+ verb := "Migrating"
+ switch mode {
+ case migrate.ModeRollback:
+ verb = "Rolling back"
+ case migrate.ModeClean:
+ verb = "Cleaning"
+ }
+ return &progress{verb: verb}
+}
+
+func (p *progress) start(msg string) {
+ if p == nil {
+ return
+ }
+ p.stop()
+ p.spin = cmdutil.StartSpinner(msg)
+}
+
+// begin starts the spinner for the run itself.
+func (p *progress) begin() {
+ if p != nil {
+ p.start(p.verb + "…")
+ }
+}
+
+func (p *progress) update(label string, done, total int) {
+ if p == nil || p.spin == nil {
+ return
+ }
+ p.spin.Update(fmt.Sprintf("%s %s %d/%d", p.verb, label, done, total))
+}
+
+func (p *progress) stop() {
+ if p == nil || p.spin == nil {
+ return
+ }
+ p.spin.Stop()
+ p.spin = nil
+}
+
+// callback returns the migrator Progress hook, or nil when there is no spinner.
+func (p *progress) callback() func(string, int, int) {
+ if p == nil {
+ return nil
+ }
+ return p.update
+}
diff --git a/cmd/cli/migrate/prune.go b/cmd/cli/migrate/prune.go
new file mode 100644
index 000000000..ffd7d2494
--- /dev/null
+++ b/cmd/cli/migrate/prune.go
@@ -0,0 +1,110 @@
+//go:build !runtime
+
+package migrate
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/kubeclient"
+ "github.com/intentd/inrun/pkg/migrate"
+ "github.com/intentd/inrun/pkg/types"
+ xterm "golang.org/x/term"
+)
+
+// confirmPrune asks before a forward run deletes source CRs. It dry-runs the
+// migration to count the sources each kind would prune, shows the counts and
+// asks [y/N] on stderr. It returns false when the user declines. Without a
+// terminal it fails and asks for --yes instead of waiting for input.
+func confirmPrune(ctx context.Context, bp *blueprint.Blueprint, kube kubeclient.Interface, funcs types.KELFunctionRegistry, opts migrate.MigratorOpts, prog *progress) (bool, error) {
+ if !declaresPrune(bp, opts.Kind) {
+ return true, nil
+ }
+
+ preview := opts
+ preview.DryRun = true
+ prog.start("Checking what prune would delete…")
+ results, err := migrate.New(ctx, kube, funcs, migrate.ModeForward, preview).Run(ctx, bp)
+ prog.stop()
+ if err != nil {
+ return false, err
+ }
+
+ var lines []string
+ total := 0
+ for _, r := range results {
+ n := 0
+ for _, cr := range r.CRs {
+ if cr.Err == nil && cr.Pruned {
+ n++
+ }
+ }
+ if n == 0 {
+ continue
+ }
+ total += n
+ lines = append(lines, fmt.Sprintf(" %s → %s %s", cmdutil.Bold(r.Kind), cmdutil.Bold(r.To), cmdutil.Yellow(plural(n, "source"))))
+ }
+ if total == 0 {
+ return true, nil
+ }
+
+ fmt.Fprintln(os.Stderr)
+ fmt.Fprintln(os.Stderr, cmdutil.Yellow("⚠ migrate.prune is on.")+" These source CRs are deleted after their destination is written:")
+ for _, l := range lines {
+ fmt.Fprintln(os.Stderr, l)
+ }
+ fmt.Fprintln(os.Stderr)
+
+ if !stdinIsTerminal() {
+ return false, fmt.Errorf("prune needs confirmation: rerun with --yes, or --dry-run to preview")
+ }
+ return askYes(os.Stdin, fmt.Sprintf("Delete %s after migrating? [y/N]: ", plural(total, "source CR")))
+}
+
+// declaresPrune reports whether any CRD in scope declares migrate.prune.
+func declaresPrune(bp *blueprint.Blueprint, kind string) bool {
+ for _, name := range bp.CRDNames() {
+ entry, ok := bp.CRDEntry(name)
+ if !ok || !entry.HasMigrate() || !entry.Migrate.Prune {
+ continue
+ }
+ if kind == "" || entry.GVK().Kind == kind {
+ return true
+ }
+ }
+ return false
+}
+
+// askYes prints prompt to stderr and reports whether the answer is y or yes.
+func askYes(in io.Reader, prompt string) (bool, error) {
+ fmt.Fprint(os.Stderr, prompt)
+ line, err := bufio.NewReader(in).ReadString('\n')
+ if err != nil && err != io.EOF {
+ return false, fmt.Errorf("read answer: %w", err)
+ }
+ switch strings.ToLower(strings.TrimSpace(line)) {
+ case "y", "yes":
+ return true, nil
+ }
+ return false, nil
+}
+
+// stdinIsTerminal reports whether stdin is a TTY. A char-device check is
+// not enough: /dev/null is a char device too.
+func stdinIsTerminal() bool {
+ return xterm.IsTerminal(int(os.Stdin.Fd()))
+}
+
+func plural(n int, noun string) string {
+ if n == 1 {
+ return fmt.Sprintf("1 %s", noun)
+ }
+ return fmt.Sprintf("%d %ss", n, noun)
+}
diff --git a/cmd/cli/migrate/prune_test.go b/cmd/cli/migrate/prune_test.go
new file mode 100644
index 000000000..4cdc6d2d0
--- /dev/null
+++ b/cmd/cli/migrate/prune_test.go
@@ -0,0 +1,23 @@
+//go:build !runtime
+
+package migrate
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestAskYes(t *testing.T) {
+ for in, want := range map[string]bool{
+ "y\n": true, "Y\n": true, "yes\n": true, " YES \n": true,
+ "n\n": false, "\n": false, "": false, "maybe\n": false,
+ } {
+ got, err := askYes(strings.NewReader(in), "")
+ if err != nil {
+ t.Fatalf("%q: %v", in, err)
+ }
+ if got != want {
+ t.Errorf("askYes(%q) = %v, want %v", in, got, want)
+ }
+ }
+}
diff --git a/cmd/cli/migrate/rollback.go b/cmd/cli/migrate/rollback.go
new file mode 100644
index 000000000..3b8f6c6b0
--- /dev/null
+++ b/cmd/cli/migrate/rollback.go
@@ -0,0 +1,40 @@
+//go:build !runtime
+
+package migrate
+
+import (
+ "github.com/intentd/inrun/pkg/migrate"
+ "github.com/spf13/cobra"
+)
+
+var rollbackCmd = &cobra.Command{
+ Use: "rollback",
+ Short: "Roll back CRs to their pre-migration state using the stored snapshot",
+ Long: `Reverse a schema migration by restoring the field values captured in the
+inrun.dev/migrate-field-snapshot annotation. For a migrate.to migration, the
+destination's inrun.dev/migrate-kind-snapshot is used: the source is recreated
+if it was pruned, then the destination is deleted.
+
+For each CR with a snapshot annotation, the rollback:
+ 1. reads the recorded {before, after} entry for each field from the snapshot
+ 2. compares the current spec value at the new path (path:) to the recorded
+ after value — if they differ the field was modified after migration and is
+ left untouched (reported as drifted)
+ 3. writes the before value back to the old path (migrate.from)
+ 4. nulls out the new path (path:) and removes the migration annotations
+
+CRs without a snapshot annotation are skipped. Rollback is idempotent.
+Requires a live cluster connection.
+
+Example:
+ inrun migrate rollback -f blueprint.yaml
+ inrun migrate rollback -f blueprint.yaml --dry-run
+ inrun migrate rollback -f blueprint.yaml --namespace platform-01 --name my-app`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runMigrateCmd(cmd, migrate.ModeRollback)
+ },
+}
+
+func init() {
+ migrateCmd.AddCommand(rollbackCmd) // parent, not RootCmd
+}
diff --git a/cmd/cli/notes/notes.go b/cmd/cli/notes/notes.go
deleted file mode 100644
index 0b2b2edbe..000000000
--- a/cmd/cli/notes/notes.go
+++ /dev/null
@@ -1,204 +0,0 @@
-//go:build !runtime && !gateway
-
-// Package notes implements the notes command for browsing and searching the
-// template functions available in Catalog expressions.
-package notes
-
-import (
- "fmt"
- "os"
- "os/exec"
- "strings"
- "text/tabwriter"
-
- "github.com/intentd/inrun/cmd/cli/cmdutil"
-
- "github.com/intentd/inrun/pkg/merger"
- "github.com/intentd/inrun/pkg/note"
- "github.com/intentd/inrun/pkg/types"
- "github.com/spf13/cobra"
-)
-
-var notesCmd = &cobra.Command{
- Use: "notes",
- Short: "Browse and search Inrun note functions",
- Long: `List, search, and inspect Inrun notes — the template functions available
-in every Catalog expression.
-
- inrun notes list all notes
- inrun notes --domain strings filter by domain
- inrun notes search full-text search
- inrun notes show show full detail for one note`,
- RunE: func(cmd *cobra.Command, args []string) error {
- domain, _ := cmd.Flags().GetString("domain")
- noPager, _ := cmd.Flags().GetBool("no-pager")
- files, _ := cmd.Flags().GetStringSlice("file")
- expanded := cmdutil.ParseFilePaths(files)
-
- // When -f is provided, show user-defined notes from the Catalog.
- if len(expanded) > 0 {
- m := merger.New(expanded...)
- if err := m.Merge(); err != nil {
- return fmt.Errorf("loading catalog: %w", err)
- }
- reg := m.ToNotes()
- if reg.Empty() {
- fmt.Println("No user-defined notes declared in this Catalog.")
- return nil
- }
- return printUserNoteTable(reg, noPager)
- }
-
- var notes []note.NoteInfo
- if domain != "" {
- notes = note.ListByDomain(domain)
- if len(notes) == 0 {
- return fmt.Errorf("no notes found for domain %q — run `inrun notes` to see available domains", domain)
- }
- } else {
- notes = note.ListNotes()
- }
- return printNoteTable(notes, noPager)
- },
-}
-
-var notesSearchCmd = &cobra.Command{
- Use: "search ",
- Short: "Search notes by name, description, or example",
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- noPager, _ := cmd.Flags().GetBool("no-pager")
- results := note.SearchNotes(args[0])
- if len(results) == 0 {
- fmt.Printf("no notes match %q\n", args[0])
- return nil
- }
- fmt.Printf("Found %d note(s) matching %q:\n\n", len(results), args[0])
- return printNoteTable(results, noPager)
- },
-}
-
-var notesShowCmd = &cobra.Command{
- Use: "show ",
- Short: "Show full documentation for a single note",
- Args: cobra.ExactArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- noPager, _ := cmd.Flags().GetBool("no-pager")
- n, ok := note.GetNote(args[0])
- if !ok {
- return fmt.Errorf("note %q not found — run `inrun notes search %s` to find similar notes", args[0], args[0])
- }
- return printNoteDetail(n, noPager)
- },
-}
-
-var notesDomainCmd = &cobra.Command{
- Use: "domains",
- Short: "List available note domains",
- RunE: func(cmd *cobra.Command, args []string) error {
- domains := note.Domains()
- fmt.Println("Available domains:")
- fmt.Println()
- for _, d := range domains {
- notes := note.ListByDomain(d)
- fmt.Printf(" %-16s %d notes\n", d, len(notes))
- }
- return nil
- },
-}
-
-func init() {
- notesCmd.Flags().StringP("domain", "d", "", "Filter by domain (e.g. strings, cron, kubernetes)")
- notesCmd.Flags().Bool("no-pager", false, "Print directly without paging")
- notesSearchCmd.Flags().Bool("no-pager", false, "Print directly without paging")
- notesShowCmd.Flags().Bool("no-pager", false, "Print directly without paging")
-
- notesCmd.AddCommand(notesSearchCmd)
- notesCmd.AddCommand(notesShowCmd)
- notesCmd.AddCommand(notesDomainCmd)
- cmdutil.RootCmd.AddCommand(notesCmd)
-
- // Shadow global flags
- cmdutil.ShadowGlobalCommandFlags(notesCmd)
-}
-
-// ── Rendering ─────────────────────────────────────────────────────────────────
-
-func printNoteTable(notes []note.NoteInfo, noPager bool) error {
- var sb strings.Builder
- w := tabwriter.NewWriter(&sb, 0, 0, 3, ' ', 0)
- fmt.Fprintln(w, "DOMAIN\tNAME\tDESCRIPTION")
- fmt.Fprintln(w, "──────\t────\t───────────")
- for _, n := range notes {
- desc := n.Description
- if len(desc) > 72 {
- desc = desc[:69] + "..."
- }
- fmt.Fprintf(w, "%s\t%s\t%s\n", n.Domain, n.Name, desc)
- }
- w.Flush()
- return page(sb.String(), noPager)
-}
-
-func printNoteDetail(n note.NoteInfo, noPager bool) error {
- sep := strings.Repeat("─", 64)
- var sb strings.Builder
- fmt.Fprintln(&sb, sep)
- fmt.Fprintf(&sb, " %s\n", n.Name)
- fmt.Fprintln(&sb, sep)
- fmt.Fprintf(&sb, " Domain: %s\n", n.Domain)
- if n.Description != "" {
- fmt.Fprintf(&sb, " Description: %s\n", n.Description)
- }
- if len(n.Keywords) > 0 {
- fmt.Fprintf(&sb, " Keywords: %s\n", strings.Join(n.Keywords, ", "))
- }
- if len(n.SeeAlso) > 0 {
- fmt.Fprintf(&sb, " See also: %s\n", strings.Join(n.SeeAlso, ", "))
- }
- if n.Example != "" {
- fmt.Fprintln(&sb)
- fmt.Fprintln(&sb, " Example:")
- for _, line := range strings.Split(n.Example, "\n") {
- fmt.Fprintf(&sb, " %s\n", line)
- }
- }
- fmt.Fprintln(&sb, sep)
- return page(sb.String(), noPager)
-}
-
-// page writes output through `less` when stdout is a terminal, otherwise prints directly.
-func page(content string, noPager bool) error {
- if noPager || !cmdutil.IsTerminal() {
- fmt.Print(content)
- return nil
- }
- pager := exec.Command("less", "-RFX")
- pager.Stdin = strings.NewReader(content)
- pager.Stdout = os.Stdout
- pager.Stderr = os.Stderr
- if err := pager.Run(); err != nil {
- fmt.Print(content)
- }
- return nil
-}
-
-func printUserNoteTable(reg types.NoteRegistry, noPager bool) error {
- var sb strings.Builder
- w := tabwriter.NewWriter(&sb, 0, 0, 3, ' ', 0)
- fmt.Fprintln(w, "NAME\tEXPRESSION\tDESCRIPTION")
- fmt.Fprintln(w, "────\t──────────\t───────────")
- for _, n := range reg.Functions {
- expr := n.Expression
- if len(expr) > 60 {
- expr = expr[:57] + "..."
- }
- desc := n.Description
- if len(desc) > 50 {
- desc = desc[:47] + "..."
- }
- fmt.Fprintf(w, "%s\t%s\t%s\n", n.Name, expr, desc)
- }
- w.Flush()
- return page(sb.String(), noPager)
-}
diff --git a/cmd/cli/registry/inspect.go b/cmd/cli/registry/inspect.go
index 99c7a2395..dfdce07ef 100644
--- a/cmd/cli/registry/inspect.go
+++ b/cmd/cli/registry/inspect.go
@@ -4,6 +4,7 @@ package registry
import (
"fmt"
+ "os"
"strings"
"time"
@@ -25,9 +26,9 @@ var inspectCmd = &cobra.Command{
Example: ` inrun inspect postgres:v14
inrun inspect web-service:v1.0.0 --module
inrun inspect oci://ghcr.io/myorg/patterns/redis:v7
- inrun inspect redis:v1.0.0 --view catalog.yaml,simulate.yaml`,
+ inrun inspect redis:v1.0.0 --view blueprint.yaml,simulate.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
- kind := registry.CatalogKind
+ kind := registry.BlueprintKind
if inspectModule {
kind = registry.ModuleKind
}
@@ -77,7 +78,7 @@ var inspectCmd = &cobra.Command{
}
if v.Meta.Deprecated != nil {
dep := v.Meta.Deprecated
- d := &types.CatalogDeprecation{
+ d := &types.BlueprintDeprecation{
MigratedTo: dep.MigratedTo,
Message: dep.Message,
}
@@ -159,20 +160,31 @@ var inspectCmd = &cobra.Command{
fileMap[f.Name] = f
available = append(available, f.Name)
}
+ // The files print as one YAML stream ("---" between them) so the output
+ // can be piped to kubectl apply -f -. Problems go to stderr, out of the stream.
+ printed := 0
for _, name := range strings.Split(viewArg, ",") {
name = strings.TrimSpace(name)
+ // cr.yaml finds manifests/cr.yaml, e2e.yaml finds test/e2e.yaml.
+ if rel := registry.ResolvePatternFile(name, func(rel string) bool { _, ok := fileMap[rel]; return ok }); rel != "" {
+ name = rel
+ }
f, ok := fileMap[name]
if !ok {
- fmt.Printf(" %s %q not in artifact (available: %s)\n", cmdutil.WarningMark(), name, strings.Join(available, ", "))
+ fmt.Fprintf(os.Stderr, " %s %q not in artifact (available: %s)\n", cmdutil.WarningMark(), name, strings.Join(available, ", "))
continue
}
- fmt.Printf("# ── %s ──\n", name)
data, err := client.ViewFile(cmd.Context(), ref, f)
if err != nil {
- fmt.Printf(" error: %v\n", err)
+ fmt.Fprintf(os.Stderr, " error reading %s: %v\n", name, err)
continue
}
- fmt.Println(string(data))
+ if printed > 0 {
+ fmt.Println("---")
+ }
+ fmt.Printf("# ── %s ──\n", name)
+ fmt.Println(strings.TrimRight(string(data), "\n"))
+ printed++
}
return nil
}
@@ -181,7 +193,7 @@ var inspectCmd = &cobra.Command{
if m.Deprecated != nil {
cmdutil.PrintPatternDeprecation(m.Deprecated)
dep := m.Deprecated
- d := &types.CatalogDeprecation{
+ d := &types.BlueprintDeprecation{
MigratedTo: dep.MigratedTo,
Message: dep.Message,
}
@@ -210,7 +222,7 @@ var inspectCmd = &cobra.Command{
fmt.Printf(" Size: %s\n", cmdutil.FormatSize(info.Size))
fmt.Printf("\n Description: %s\n", cmdutil.WordWrap(m.Description, 55, " "))
if len(m.Tags) > 0 {
- fmt.Printf(" Tags: %s\n", strings.Join(m.Tags, ", "))
+ fmt.Printf(" Tags: %s\n", cmdutil.WordWrap(strings.Join(m.Tags, ", "), 55, " "))
}
if m.Author != "" {
fmt.Printf(" Author: %s\n", m.Author)
@@ -321,6 +333,10 @@ var inspectCmd = &cobra.Command{
if m.RuntimeVersion != "" {
fmt.Printf(" Runtime: %s\n", m.RuntimeVersion)
}
+ if m.Replaces != "" {
+ fmt.Printf(" Replaces: %s\n", m.Replaces)
+ fmt.Printf(" %s\n", cmdutil.Yellow("an earlier "+m.Version+", pushed over with --overwrite"))
+ }
if len(info.Files) > 0 {
fmt.Printf("\n Files:\n")
for _, f := range info.Files {
@@ -328,10 +344,12 @@ var inspectCmd = &cobra.Command{
}
}
fmt.Printf("\nTo pull:\n")
+ // The reference as inspected: a digest or another registry pulls exactly
+ // this artifact, which name:version alone would not.
if m.Kind == registry.ModuleKind {
- fmt.Printf(" inrun pull %s:%s --module %s\n", m.Name, m.Version, deprecated)
+ fmt.Printf(" inrun pull %s --module %s\n", ref.String(), deprecated)
} else {
- fmt.Printf(" inrun pull %s:%s %s\n", m.Name, m.Version, deprecated)
+ fmt.Printf(" inrun pull %s %s\n", ref.String(), deprecated)
}
fmt.Printf("\nTo import:\n")
if m.Kind == registry.ModuleKind {
@@ -349,7 +367,7 @@ var inspectCmd = &cobra.Command{
func init() {
inspectCmd.Flags().BoolVarP(&inspectModule, "module", "m", false, "Resolve as a module (uses INRUN_MODULES_REGISTRY)")
- inspectCmd.Flags().String("view", "", "Comma-separated list of files to print before pulling (e.g. catalog.yaml,cr.yaml)")
+ inspectCmd.Flags().String("view", "", "Comma-separated list of files to print before pulling (e.g. blueprint.yaml,cr.yaml)")
inspectCmd.Flags().Bool("versions", false, "List up to 10 tracked versions with simulate and E2E status")
cmdutil.RootCmd.AddCommand(inspectCmd)
diff --git a/cmd/cli/registry/patterns.go b/cmd/cli/registry/patterns.go
index ce37eafd9..08683a662 100644
--- a/cmd/cli/registry/patterns.go
+++ b/cmd/cli/registry/patterns.go
@@ -23,12 +23,12 @@ var patternsCmd = &cobra.Command{
Args: cobra.MaximumNArgs(1),
Example: ` inrun patterns
inrun patterns --modules
- inrun patterns --catalogs
+ inrun patterns --blueprints
inrun patterns oci://ghcr.io/mycompany/patterns
inrun patterns --tag database`,
RunE: func(cmd *cobra.Command, args []string) error {
tag, _ := cmd.Flags().GetString("tag")
- onlyCatalogs, _ := cmd.Flags().GetBool("catalogs")
+ onlyBlueprints, _ := cmd.Flags().GetBool("blueprints")
onlyModules, _ := cmd.Flags().GetBool("modules")
client, err := registry.NewClient()
@@ -63,7 +63,7 @@ var patternsCmd = &cobra.Command{
}
}
}
- if !onlyCatalogs {
+ if !onlyBlueprints {
moduleURL := os.Getenv(registry.EnvModuleRegistry)
if moduleURL == "" {
moduleURL = registry.DefaultModuleRegistry
@@ -92,8 +92,8 @@ var patternsCmd = &cobra.Command{
label := "Inrun Registry"
switch {
- case onlyCatalogs:
- label = "Inrun Catalogs"
+ case onlyBlueprints:
+ label = "Inrun Blueprints"
case onlyModules:
label = "Inrun Modules"
}
@@ -109,7 +109,7 @@ var patternsCmd = &cobra.Command{
continue
}
k := e.Kind
- if onlyCatalogs && k != registry.CatalogKind.ToString() {
+ if onlyBlueprints && k != registry.BlueprintKind.ToString() {
continue
}
if onlyModules && k != registry.ModuleKind.ToString() {
@@ -152,7 +152,7 @@ var patternsCmd = &cobra.Command{
fmt.Printf("\n%d %s%s\n", count, noun, updatedAt)
fmt.Printf("\nTo pull:\n inrun pull :\n")
- fmt.Printf("\nTo filter:\n inrun patterns --catalogs\n inrun patterns --modules\n")
+ fmt.Printf("\nTo filter:\n inrun patterns --blueprints\n inrun patterns --modules\n")
fmt.Println()
return nil
},
@@ -169,7 +169,7 @@ func registryErrSummary(err error) string {
func init() {
patternsCmd.Flags().StringP("tag", "t", "", "Filter by tag (e.g. database, stateful, security)")
- patternsCmd.Flags().BoolP("catalogs", "k", false, "Show only catalogs (kind: Catalog)")
+ patternsCmd.Flags().BoolP("blueprints", "k", false, "Show only blueprints (kind: Blueprint)")
patternsCmd.Flags().BoolP("modules", "m", false, "Show only modules (kind: Module)")
cmdutil.RootCmd.AddCommand(patternsCmd)
diff --git a/cmd/cli/registry/pull.go b/cmd/cli/registry/pull.go
index 23ff4f1fc..addb11fb7 100644
--- a/cmd/cli/registry/pull.go
+++ b/cmd/cli/registry/pull.go
@@ -7,12 +7,13 @@ import (
"os"
"strings"
+ "github.com/intentd/inrun/pkg/util/fileio"
+
"github.com/intentd/inrun/cmd/cli/cmdutil"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/registry"
"github.com/intentd/inrun/pkg/registry/module"
- "github.com/intentd/inrun/pkg/utils"
"github.com/spf13/cobra"
)
@@ -24,7 +25,7 @@ var pullCmd = &cobra.Command{
Args: cobra.RangeArgs(0, 1),
Example: ` inrun pull postgres:v14
inrun pull oci://ghcr.io/myorg/patterns/redis:v7
- inrun pull -f catalog.yaml
+ inrun pull -f blueprint.yaml
inrun pull -f stack.yaml
inrun pull postgres:v14 --refresh`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -37,11 +38,11 @@ var pullCmd = &cobra.Command{
}
if len(args) == 0 {
- return fmt.Errorf("provide a reference (e.g. postgres:v14) or --file ")
+ return fmt.Errorf("provide a reference (e.g. postgres:v14) or --file ")
}
isModule, _ := cmd.Flags().GetBool("module")
- kind := registry.CatalogKind
+ kind := registry.BlueprintKind
if isModule {
kind = registry.ModuleKind
}
@@ -106,7 +107,7 @@ var pullCmd = &cobra.Command{
func init() {
pullCmd.Flags().Bool("refresh", false, "Bypass local cache and re-pull from registry")
pullCmd.Flags().StringP("out", "o", "", "Extract pulled pattern to this directory")
- pullCmd.Flags().StringP("file", "f", "", "Pull all OCI imports from a catalog or stack file")
+ pullCmd.Flags().StringP("file", "f", "", "Pull all OCI imports from a blueprint or stack file")
pullCmd.Flags().BoolP("module", "m", false, "Resolve as a module (uses INRUN_MODULES_REGISTRY)")
cmdutil.RootCmd.AddCommand(pullCmd)
@@ -124,15 +125,15 @@ func notifyTypedPull(cacheDir string) {
cmdutil.PrintTypedBuildSteps(hasMakefile == nil)
}
-// pullModuleDeps reads the catalog.yaml in cacheDir and pulls any OCI module
+// pullModuleDeps reads the blueprint.yaml in cacheDir and pulls any OCI module
// imports it declares. Warnings are printed but do not fail the main pull.
-func pullModuleDeps(catalogCacheDir string) {
- catalogFile := cmdutil.JoinPath(catalogCacheDir, registry.FileCatalog)
- if _, err := os.Stat(catalogFile); err != nil {
+func pullModuleDeps(blueprintCacheDir string) {
+ blueprintFile := cmdutil.JoinPath(blueprintCacheDir, registry.FileBlueprint)
+ if _, err := os.Stat(blueprintFile); err != nil {
return
}
- imports, err := registry.ExtractOCIImports(catalogFile)
+ imports, err := registry.ExtractOCIImports(blueprintFile)
if err != nil || len(imports.ModuleImports) == 0 {
return
}
@@ -154,8 +155,8 @@ func pullModuleDeps(catalogCacheDir string) {
}
}
-// pullFromFile extracts all OCI refs from a catalog or stack file and pulls
-// each one. Module imports (Catalog) and registry imports (Stack) are both
+// pullFromFile extracts all OCI refs from a blueprint or stack file and pulls
+// each one. Module imports (Blueprint) and registry imports (Stack) are both
// handled, including bare-name shorthands without an oci:// prefix.
func pullFromFile(cmd *cobra.Command, filePath string, refresh bool) error {
imports, err := registry.ExtractOCIImports(filePath)
@@ -231,9 +232,9 @@ func pullFromFile(cmd *cobra.Command, filePath string, refresh bool) error {
for _, url := range helmImports.RemoteFiles {
spin := cmdutil.StartSpinner(url)
if refresh {
- utils.InvalidateFileCache(url)
+ fileio.InvalidateFileCache(url)
}
- if _, fetchErr := utils.LoadFileWithAuthRefresh(url, nil, refresh); fetchErr != nil {
+ if _, fetchErr := fileio.LoadFileWithAuthRefresh(url, nil, refresh); fetchErr != nil {
spin.Failure()
errs = append(errs, fmt.Sprintf("file %s: %v", url, fetchErr))
} else {
diff --git a/cmd/cli/registry/pull_print.go b/cmd/cli/registry/pull_print.go
index 2117ff17b..5ad5d1823 100644
--- a/cmd/cli/registry/pull_print.go
+++ b/cmd/cli/registry/pull_print.go
@@ -15,15 +15,15 @@ import (
)
// detectCacheType resolves the artifact kind by checking for sentinel files on
-// disk — not by sniffing the path string. Returns (isModule, isCatalog, patternFile).
+// disk — not by sniffing the path string. Returns (isModule, isBlueprint, patternFile).
func detectCacheType(cacheDir string) (bool, bool, string) {
moduleFile := filepath.Join(cacheDir, registry.FileModule)
if _, err := os.Stat(moduleFile); err == nil {
return true, false, moduleFile
}
- catalogFile := filepath.Join(cacheDir, registry.FileCatalog)
- if _, err := os.Stat(catalogFile); err == nil {
- return false, true, catalogFile
+ blueprintFile := filepath.Join(cacheDir, registry.FileBlueprint)
+ if _, err := os.Stat(blueprintFile); err == nil {
+ return false, true, blueprintFile
}
return false, false, ""
}
@@ -82,7 +82,7 @@ func printValidationHint(patternFile string) {
// printModuleReference prints module usage and inputs sample.
func printModuleReference(ref *registry.Ref, module *types.Module, sum inputsSummary) {
- fmt.Printf("\nReference in a Catalog:\n")
+ fmt.Printf("\nReference in a Blueprint:\n")
fmt.Printf(" imports:\n")
fmt.Printf(" - module: %s\n", ref.String())
@@ -111,10 +111,10 @@ func printModuleReference(ref *registry.Ref, module *types.Module, sum inputsSum
}
}
-// printCatalogReference prints catalog usage and run hint.
-func printCatalogReference(ref *registry.Ref, cacheDir string) {
- fmt.Printf("\nRun this catalog pattern:\n")
- fmt.Printf(" inrun -f %s\n", filepath.Join(cacheDir, registry.FileCatalog))
+// printBlueprintReference prints blueprint usage and run hint.
+func printBlueprintReference(ref *registry.Ref, cacheDir string) {
+ fmt.Printf("\nRun this blueprint pattern:\n")
+ fmt.Printf(" inrun -f %s\n", filepath.Join(cacheDir, registry.FileBlueprint))
fmt.Printf("\nOr reference in a Stack:\n")
fmt.Printf(" imports:\n")
fmt.Printf(" registry:\n")
@@ -143,7 +143,7 @@ func printCachedFiles(cacheDir string) {
// printPullSuggestions orchestrates the helpers and handles errors gracefully.
func printPullSuggestions(ref *registry.Ref, cacheDir string) {
- isModule, isCatalog, patternFile := detectCacheType(cacheDir)
+ isModule, isBlueprint, patternFile := detectCacheType(cacheDir)
printCachedFiles(cacheDir)
printValidationHint(patternFile)
@@ -159,11 +159,11 @@ func printPullSuggestions(ref *registry.Ref, cacheDir string) {
return
}
- if isCatalog {
- printCatalogReference(ref, cacheDir)
+ if isBlueprint {
+ printBlueprintReference(ref, cacheDir)
return
}
// fallback: nothing recognized
- fmt.Println("No module or catalog pattern detected in cache.")
+ fmt.Println("No module or blueprint pattern detected in cache.")
}
diff --git a/cmd/cli/registry/push.go b/cmd/cli/registry/push.go
index 23e12f7ca..e60fe0a91 100644
--- a/cmd/cli/registry/push.go
+++ b/cmd/cli/registry/push.go
@@ -4,6 +4,7 @@ package registry
import (
"bufio"
+ "context"
"fmt"
"os"
"path/filepath"
@@ -15,8 +16,8 @@ import (
"github.com/intentd/inrun/cmd/cli/serve"
"github.com/intentd/inrun/cmd/cli/testsuite"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/registry"
"github.com/intentd/inrun/pkg/registry/e2e"
@@ -36,6 +37,11 @@ var (
pushE2ECluster string
pushE2EUseCurrent bool
pushE2EWorkers int
+ pushE2EChart string
+ pushE2EVersion string
+ pushE2EValues []string
+ pushE2ESet []string
+ pushOverwrite bool
pushAddIntent string
)
@@ -116,11 +122,20 @@ var pushCmd = &cobra.Command{
return fmt.Errorf("invalid reference: %w", err)
}
+ // A published version is never replaced by accident: consumers who pulled
+ // it, and the proof recorded for it, refer to what was there. Checked
+ // before the gates, so a refused push doesn't wait for them first. With
+ // --overwrite, the new artifact records the digest it replaces.
+ replaces, err := checkPublished(cmd.Context(), ref)
+ if err != nil {
+ return err
+ }
+
cmdutil.PrintBanner()
fmt.Printf("Pushing %s (%s) to %s...\n", refArg, patternKind, ref.Registry)
- if patternKind == registry.CatalogKind {
- localImports, err := registry.ExtractLocalModuleImports(filepath.Join(dir, registry.FileCatalog))
+ if patternKind == registry.BlueprintKind {
+ localImports, err := registry.ExtractLocalModuleImports(filepath.Join(dir, registry.FileBlueprint))
if err == nil && len(localImports) > 0 {
var lines []string
for _, li := range localImports {
@@ -129,29 +144,29 @@ var pushCmd = &cobra.Command{
return fmt.Errorf(
"✗ Push blocked: local file imports in %s\n\n%s\n\n"+
" Local imports work for inrun simulate and inrun template, but cannot\n"+
- " be resolved by consumers after the catalog is published.\n\n"+
+ " be resolved by consumers after the blueprint is published.\n\n"+
" Before publishing:\n"+
" 1. Push the module: inrun push /\n"+
" 2. Replace the local path with the OCI ref:\n"+
" module: oci:///modules/:",
- registry.FileCatalog,
+ registry.FileBlueprint,
strings.Join(lines, "\n"),
)
}
}
- if patternKind == registry.CatalogKind {
- m := merger.New(filepath.Join(dir, registry.FileCatalog))
+ if patternKind == registry.BlueprintKind {
+ m := merger.New(filepath.Join(dir, registry.FileBlueprint))
if err := m.Merge(); err != nil {
- return fmt.Errorf(" ✗ %s: %w", registry.FileCatalog, err)
+ return fmt.Errorf(" ✗ %s: %w", registry.FileBlueprint, err)
}
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
if err != nil {
- return fmt.Errorf(" ✗ %s: %w", registry.FileCatalog, err)
+ return fmt.Errorf(" ✗ %s: %w", registry.FileBlueprint, err)
}
- fmt.Printf(" %s %-20s valid\n", cmdutil.SuccessMark(), registry.FileCatalog)
- if d := cat.Deprecation(); d != nil {
- cmdutil.PrintCatalogDeprecation(d)
+ fmt.Printf(" %s %-20s valid\n", cmdutil.SuccessMark(), registry.FileBlueprint)
+ if d := bp.Deprecation(); d != nil {
+ cmdutil.PrintBlueprintDeprecation(d)
}
if crd := registry.FindPatternFile(dir, registry.FileCRD); crd != "" && slices.Contains(files, crd) {
@@ -163,7 +178,7 @@ var pushCmd = &cobra.Command{
}
for _, f := range files {
- if f == registry.FileCatalog || filepath.Base(f) == registry.FileCRD {
+ if f == registry.FileBlueprint || filepath.Base(f) == registry.FileCRD {
continue
}
info, _ := os.Stat(filepath.Join(dir, f))
@@ -171,7 +186,7 @@ var pushCmd = &cobra.Command{
}
var simulateMeta *registry.PatternSimulate
- if patternKind == registry.CatalogKind {
+ if patternKind == registry.BlueprintKind {
if rel := registry.FindPatternFile(dir, registry.FileSimulate); rel != "" {
simFile := filepath.Join(dir, rel)
if pushForce || pushNoSimulate {
@@ -208,7 +223,7 @@ var pushCmd = &cobra.Command{
}
var e2eMeta *registry.PatternE2E
- if patternKind == registry.CatalogKind {
+ if patternKind == registry.BlueprintKind {
e2eFile := pushE2EFile
if e2eFile == "" {
if rel := registry.FindPatternFile(dir, registry.FileE2E); rel != "" {
@@ -227,7 +242,14 @@ var pushCmd = &cobra.Command{
}
} else {
fmt.Printf("\nRunning E2E gate (%s)...\n", registry.FileE2E)
- runner, err := e2e.New(e2eFile, e2e.Options{ClusterCtx: pushE2ECluster, UseCurrentCtx: pushE2EUseCurrent, Workers: pushE2EWorkers})
+ chart, err := cmdutil.ChartDir(pushE2EChart)
+ if err != nil {
+ return err
+ }
+ runner, err := e2e.New(e2eFile, e2e.Options{
+ ClusterCtx: pushE2ECluster, UseCurrentCtx: pushE2EUseCurrent, Workers: pushE2EWorkers,
+ InrunVersion: pushE2EVersion, InrunChart: chart, ValueFiles: pushE2EValues, HelmArgs: cmdutil.HelmSetArgs(pushE2ESet),
+ })
if err != nil {
return fmt.Errorf("e2e gate: %w\n\nUse --force or --no-e2e to skip", err)
}
@@ -258,13 +280,13 @@ var pushCmd = &cobra.Command{
}
var intentMeta *registry.PatternIntent
- if patternKind == registry.CatalogKind && pushAddIntent != "" {
+ if patternKind == registry.BlueprintKind && pushAddIntent != "" {
intentFile := pushAddIntent
if !filepath.IsAbs(intentFile) {
intentFile = filepath.Join(dir, intentFile)
}
fmt.Printf("\nRunning intent play (%s)...\n", pushAddIntent)
- target, perr := serve.RunIntentPlay(filepath.Join(dir, registry.FileCatalog), intentFile)
+ target, perr := serve.RunIntentPlay(filepath.Join(dir, registry.FileBlueprint), intentFile)
status := "passed"
if perr != nil {
status = "failed"
@@ -280,8 +302,8 @@ var pushCmd = &cobra.Command{
}
var typedMeta *registry.PatternTyped
- if patternKind == registry.CatalogKind {
- typedMeta = detectTypedCatalog(filepath.Join(dir, registry.FileCatalog))
+ if patternKind == registry.BlueprintKind {
+ typedMeta = detectTypedBlueprint(filepath.Join(dir, registry.FileBlueprint))
}
runtimeVersion := version.Short()
@@ -307,6 +329,7 @@ var pushCmd = &cobra.Command{
Intent: intentMeta,
Typed: typedMeta,
RuntimeVersion: runtimeVersion,
+ Replaces: replaces,
}
digest, err := client.Push(cmd.Context(), ref, dir, opts, progress)
if err != nil {
@@ -318,14 +341,20 @@ var pushCmd = &cobra.Command{
fmt.Printf("\n%s Pushed: %s\n", cmdutil.SuccessMark(), ref.String())
fmt.Printf(" Digest: %s\n", digest)
- if patternKind == registry.CatalogKind {
+ if patternKind == registry.BlueprintKind {
moduleYAML := filepath.Join(dir, registry.FileModule)
if _, err := os.Stat(moduleYAML); err == nil {
moduleRef, err := registry.ResolveForKind(fmt.Sprintf("%s:%s", meta.Name, meta.Version), registry.ModuleKind)
+ var moduleReplaces string
+ if err == nil {
+ if moduleReplaces, err = checkPublished(cmd.Context(), moduleRef); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: not pushing %s: %v\n", registry.FileModule, err)
+ }
+ }
if err == nil {
fmt.Printf("\nAlso pushing %s to %s...\n", registry.FileModule, moduleRef.Registry)
spinModule := cmdutil.StartSpinner(fmt.Sprintf("Pushing %s...", registry.FileModule))
- if mDigest, err := client.Push(cmd.Context(), moduleRef, dir, registry.PushOptions{RuntimeVersion: version.Short()}, nil); err != nil {
+ if mDigest, err := client.Push(cmd.Context(), moduleRef, dir, registry.PushOptions{RuntimeVersion: version.Short(), Replaces: moduleReplaces}, nil); err != nil {
spinModule.Failure()
fmt.Fprintf(os.Stderr, "warning: module push failed: %v\n", err)
} else {
@@ -337,7 +366,7 @@ var pushCmd = &cobra.Command{
}
}
- fmt.Printf("\nTo import in a Catalog:\n")
+ fmt.Printf("\nTo import in a Blueprint:\n")
if patternKind == registry.ModuleKind {
fmt.Printf(" imports:\n")
fmt.Printf(" - module: %s\n", ref.String())
@@ -361,6 +390,11 @@ func init() {
pushCmd.Flags().StringVar(&pushE2ECluster, "cluster", "", "Reuse an existing kind cluster context for the e2e gate (skips cluster creation)")
pushCmd.Flags().BoolVar(&pushE2EUseCurrent, "use-current", false, "Use the current kubeconfig context for the e2e gate (skips cluster creation)")
pushCmd.Flags().IntVar(&pushE2EWorkers, "workers", 0, "Number of kind worker nodes for the e2e gate cluster (0 = control-plane only)")
+ pushCmd.Flags().BoolVar(&pushOverwrite, "overwrite", false, "Replace a version that is already published (anyone who pulled it keeps the old content)")
+ pushCmd.Flags().StringVar(&pushE2EChart, "chart", "", "Local Inrun chart directory for the e2e gate to install instead of the published chart")
+ pushCmd.Flags().StringVar(&pushE2EVersion, "version", "", "Inrun version for the e2e gate to install (e.g. v1.2.3)")
+ pushCmd.Flags().StringSliceVar(&pushE2EValues, "values", nil, "Helm values files for the e2e gate's Inrun install")
+ pushCmd.Flags().StringSliceVar(&pushE2ESet, "set", nil, "Helm --set arguments for the e2e gate's Inrun install (e.g. key=value)")
pushCmd.Flags().StringVar(&pushAddIntent, "add-intent", "", "Run inrun serve play against this intent file (YAML or JSON) and bake the result into the artifact")
cmdutil.RootCmd.AddCommand(pushCmd)
@@ -368,7 +402,7 @@ func init() {
cmdutil.ShadowGlobalCommandFlags(pushCmd, "file")
}
-// detectTypedCatalog parses a catalog.yaml and returns a PatternTyped if any
+// detectTypedBlueprint parses a blueprint.yaml and returns a PatternTyped if any
// CRD declares customHooks or customConstructor. Returns nil on parse error.
// extractRuntimeVersionFromGoMod scans go.mod for the inrun runtime dependency
// and returns its version (e.g. "v0.7.6"). Returns "" if go.mod is absent or the
@@ -403,14 +437,14 @@ func extractRuntimeVersionFromGoMod(dir string) string {
return ""
}
-func detectTypedCatalog(catalogPath string) *registry.PatternTyped {
- cat, err := catalog.ParseFile(catalogPath)
+func detectTypedBlueprint(blueprintPath string) *registry.PatternTyped {
+ bp, err := blueprint.ParseFile(blueprintPath)
if err != nil {
return nil
}
var t registry.PatternTyped
- for _, name := range cat.CRDNames() {
- entry, ok := cat.CRDEntry(name)
+ for _, name := range bp.CRDNames() {
+ entry, ok := bp.CRDEntry(name)
if !ok {
continue
}
@@ -512,3 +546,21 @@ func detectRunner() string {
return "local"
}
}
+
+// checkPublished returns the digest ref already has in the registry, for the
+// new artifact to record, or "" when ref is not published. Without
+// --overwrite, a published ref is an error.
+func checkPublished(ctx context.Context, ref *registry.Ref) (string, error) {
+ client, err := registry.NewClient()
+ if err != nil {
+ return "", err
+ }
+ current, err := client.Published(ctx, ref)
+ if err != nil {
+ return "", err
+ }
+ if current != "" && !pushOverwrite {
+ return "", fmt.Errorf("%s is already published\n Bump metadata.version to publish a change, or pass --overwrite to replace it", ref.String())
+ }
+ return current, nil
+}
diff --git a/cmd/cli/rewrite/rewrite.go b/cmd/cli/rewrite/rewrite.go
new file mode 100644
index 000000000..532de888e
--- /dev/null
+++ b/cmd/cli/rewrite/rewrite.go
@@ -0,0 +1,192 @@
+//go:build !runtime && !gateway
+
+// package rewrite implements the rewrite command, which converts a
+// controller-runtime reconciler to run on this runtime.
+package rewrite
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/intentd/inrun/cmd/cli/cmdutil"
+
+ "github.com/intentd/inrun/pkg/tools/rewrite"
+ "github.com/intentd/inrun/pkg/version"
+ "github.com/spf13/cobra"
+)
+
+var rewriteCmd = &cobra.Command{
+ Use: "rewrite ",
+ Short: "Rewrite a controller-runtime operator to Inrun",
+ Long: `Rewrites a controller-runtime reconciler to Inrun. Your Reconcile logic
+is untouched — Inrun takes over the infrastructure.
+
+Default mode (--mode toclient): zero changes to your Reconcile signature or
+call sites. SetupWithManager is removed; a two-line constructor using
+adapter.ToClient and domain.ReconcilerFrom is injected. Your reconciler
+compiles and runs inside Inrun with no other edits.
+
+ inrun rewrite ./controller/webapp_controller.go -o ./my-operator
+
+The output directory receives the rewritten file plus scaffolding:
+blueprint.yaml, simulate.yaml, e2e.yaml, go.mod, Makefile, Dockerfile.
+
+For a full rewrite to Inrun style, use --mode native.
+
+Examples:
+ inrun rewrite ./controller/webapp_controller.go -o ./my-operator
+ inrun rewrite ./controller/webapp_controller.go --mode native -o ./out
+ inrun rewrite ./controller/webapp_controller.go # prompts before replacing`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ inputPath := args[0]
+ outputDir, _ := cmd.Flags().GetString("output")
+ modulePath, _ := cmd.Flags().GetString("module")
+ operatorName, _ := cmd.Flags().GetString("name")
+ modeFlag, _ := cmd.Flags().GetString("mode")
+
+ var mode rewrite.Mode
+ switch modeFlag {
+ case "native":
+ mode = rewrite.ModeNative
+ case "toclient", "":
+ mode = rewrite.ModeToClient
+ default:
+ return fmt.Errorf("unknown --mode %q: valid values are toclient, native", modeFlag)
+ }
+
+ src, err := cmdutil.ReadLocal(inputPath)
+ if err != nil {
+ return fmt.Errorf("read %s: %w", inputPath, err)
+ }
+
+ res, err := rewrite.Apply(src, mode)
+ if err != nil {
+ return fmt.Errorf("rewrite: %w", err)
+ }
+
+ if len(res.Warnings) > 0 {
+ fmt.Fprintf(os.Stderr, "\n%s\n", cmdutil.Yellow("Warnings — review these before running:"))
+ for _, w := range res.Warnings {
+ fmt.Fprintf(os.Stderr, " %s %s\n", cmdutil.Yellow("⚠"), w)
+ }
+ fmt.Fprintln(os.Stderr)
+ }
+
+ opts := rewrite.Options{
+ ModulePath: modulePath,
+ OperatorName: operatorName,
+ InrunVersion: version.Short(),
+ }
+ files := rewrite.Generate(res, opts)
+
+ if outputDir != "" {
+ return writeOutputDir(outputDir, inputPath, res, files)
+ }
+
+ return replaceInPlace(inputPath, res, files)
+ },
+}
+
+func writeOutputDir(dir, inputPath string, res *rewrite.Result, files rewrite.Files) error {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return fmt.Errorf("create output dir: %w", err)
+ }
+
+ reconcilerDest := filepath.Join(dir, filepath.Base(inputPath))
+ if err := os.WriteFile(reconcilerDest, res.Source, 0o644); err != nil {
+ return fmt.Errorf("write reconciler: %w", err)
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Green("→"), reconcilerDest)
+
+ type namedFile struct {
+ name string
+ content string
+ }
+ generated := []namedFile{
+ {"blueprint.yaml", files.Blueprint},
+ {"test/simulate.yaml", files.Simulate},
+ {"test/e2e.yaml", files.E2E},
+ {"go.mod", files.GoMod},
+ {"Makefile", files.Makefile},
+ {"Dockerfile", files.Dockerfile},
+ {"README.md", files.README},
+ }
+ for _, f := range generated {
+ dest := filepath.Join(dir, f.name)
+ if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
+ return fmt.Errorf("create %s: %w", filepath.Dir(dest), err)
+ }
+ if err := os.WriteFile(dest, []byte(f.content), 0o644); err != nil {
+ return fmt.Errorf("write %s: %w", f.name, err)
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Green("→"), dest)
+ }
+
+ fmt.Printf("\n%s Search for %s in %s to see what needs attention.\n",
+ cmdutil.Green("✓ Done."), cmdutil.Bold("TODO(inrun rewrite)"), dir)
+ return nil
+}
+
+func replaceInPlace(inputPath string, res *rewrite.Result, files rewrite.Files) error {
+ dir := filepath.Dir(inputPath)
+
+ fmt.Printf("This will replace %s and write %s alongside it.\n",
+ cmdutil.Bold(inputPath), cmdutil.Bold("blueprint.yaml, test/simulate.yaml, test/e2e.yaml, go.mod, Makefile, Dockerfile"))
+ fmt.Printf("%s [y/N] ", cmdutil.Yellow("Continue?"))
+
+ scanner := bufio.NewScanner(os.Stdin)
+ scanner.Scan()
+ answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
+ if answer != "y" && answer != "yes" {
+ fmt.Println("Aborted.")
+ return nil
+ }
+
+ if err := os.WriteFile(inputPath, res.Source, 0o644); err != nil {
+ return fmt.Errorf("write %s: %w", inputPath, err)
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Green("→"), inputPath)
+
+ type namedFile struct {
+ name string
+ content string
+ }
+ generated := []namedFile{
+ {"blueprint.yaml", files.Blueprint},
+ {"test/simulate.yaml", files.Simulate},
+ {"test/e2e.yaml", files.E2E},
+ {"go.mod", files.GoMod},
+ {"Makefile", files.Makefile},
+ {"Dockerfile", files.Dockerfile},
+ }
+ for _, f := range generated {
+ dest := filepath.Join(dir, f.name)
+ if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
+ return fmt.Errorf("create %s: %w", filepath.Dir(dest), err)
+ }
+ if err := os.WriteFile(dest, []byte(f.content), 0o644); err != nil {
+ return fmt.Errorf("write %s: %w", f.name, err)
+ }
+ fmt.Printf(" %s %s\n", cmdutil.Green("→"), dest)
+ }
+
+ fmt.Printf("\n%s Search for %s to see what needs attention.\n",
+ cmdutil.Green("✓ Done."), cmdutil.Bold("TODO(inrun rewrite)"))
+ return nil
+}
+
+func init() {
+ cmdutil.RootCmd.AddCommand(rewriteCmd)
+
+ rewriteCmd.Flags().StringP("output", "o", "", "Write output to this directory (non-destructive; skips confirmation)")
+ rewriteCmd.Flags().String("module", "", "Go module path for the rewritten operator (e.g. github.com/myorg/my-operator)")
+ rewriteCmd.Flags().String("name", "", "Operator name in kebab-case (e.g. my-operator); derived from receiver type if omitted")
+ rewriteCmd.Flags().String("mode", "toclient", "Migration mode: toclient (default, zero Reconcile changes) or native (full rewrite)")
+
+ // Shadow global flags so they don't appear under `inrun rewrite`
+ cmdutil.ShadowGlobalCommandFlags(rewriteCmd, "file")
+}
diff --git a/cmd/cli/run/companions/companions.go b/cmd/cli/run/companions/companions.go
index b1216d8b6..77bf03ab5 100644
--- a/cmd/cli/run/companions/companions.go
+++ b/cmd/cli/run/companions/companions.go
@@ -11,34 +11,34 @@ import (
"syscall"
"github.com/intentd/inrun/cmd/cli/console"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/config"
"github.com/intentd/inrun/pkg/logger"
)
// Start starts the console next to the runtime, and the gateway too when the
-// Catalog serves (gateway API enabled and a CRD with serve enabled). Each runs
+// Blueprint serves (gateway API enabled and a CRD with serve enabled). Each runs
// as a child process, so one that fails never takes the runtime down: a
// companion that cannot start is logged and skipped. A signal that stops inrun
// is passed on, so the children stop with it.
//
-// paths are the Catalogs the gateway loads; cfg is the runtime's config, which
+// paths are the Blueprints the gateway loads; cfg is the runtime's config, which
// advertises the gateway to the console.
-func Start(ctx context.Context, cfg *config.Config, paths []string, cat *catalog.Catalog) {
+func Start(ctx context.Context, cfg *config.Config, paths []string, bp *blueprint.Blueprint, migrate bool) {
var children []*exec.Cmd
- if cat.HasServeEnabled() {
- cmd, url, err := startGateway(ctx, paths)
+ if bp.HasServeEnabled() {
+ cmd, url, err := startGateway(ctx, paths, migrate)
if err != nil {
logger.Warn().Err(err).Msg("gateway not started")
} else {
children = append(children, cmd)
- // The runtime's /catalog advertises it, so the console finds it.
+ // The runtime's /blueprint advertises it, so the console finds it.
cfg.SetGatewayEndpoint(url)
}
}
- // The console finds the gateway through the runtime's /catalog.
+ // The console finds the gateway through the runtime's /blueprint.
cmd, err := startConsole(ctx, "http://localhost:"+cfg.Health().Port())
if err != nil {
logger.Warn().Err(err).Msg("console not started")
@@ -74,9 +74,9 @@ func stopWithInrun(children []*exec.Cmd) {
}()
}
-// startGateway runs this binary's `gate run` with the same Catalogs, on
+// startGateway runs this binary's `gate run` with the same Blueprints, on
// INRUN_GATEWAY_PORT (default 8082).
-func startGateway(ctx context.Context, paths []string) (*exec.Cmd, string, error) {
+func startGateway(ctx context.Context, paths []string, migrate bool) (*exec.Cmd, string, error) {
exe, err := os.Executable()
if err != nil {
return nil, "", fmt.Errorf("locating inrun: %w", err)
@@ -86,6 +86,9 @@ func startGateway(ctx context.Context, paths []string) (*exec.Cmd, string, error
for _, p := range paths {
args = append(args, "-f", p)
}
+ if migrate {
+ args = append(args, "--migrate")
+ }
cmd := exec.CommandContext(ctx, exe, args...)
cmd.Env = append(os.Environ(), "INRUN_PORT="+port)
if err := start(cmd, "gateway"); err != nil {
diff --git a/cmd/cli/run/companions/doc.go b/cmd/cli/run/companions/doc.go
index ce122fd1a..53212f648 100644
--- a/cmd/cli/run/companions/doc.go
+++ b/cmd/cli/run/companions/doc.go
@@ -1,6 +1,6 @@
//go:build !runtime && !gateway
// Package companions starts the processes that run next to a local inrun:
-// the console always, and the gateway when the Catalog serves. They run as
+// the console always, and the gateway when the Blueprint serves. They run as
// child processes and stop with inrun.
package companions
diff --git a/cmd/cli/run/doc.go b/cmd/cli/run/doc.go
index c3d0b7386..2dfa061f3 100644
--- a/cmd/cli/run/doc.go
+++ b/cmd/cli/run/doc.go
@@ -1,4 +1,4 @@
-// Package run makes the root inrun command run a Catalog: the runtime in the
+// Package run makes the root inrun command run a Blueprint: the runtime in the
// runtime build, and in the developer build also a published pattern given as
// :, with --dev to create a kind cluster.
package run
diff --git a/cmd/cli/run/inrun.go b/cmd/cli/run/inrun.go
index 7146007e0..bbc27c316 100644
--- a/cmd/cli/run/inrun.go
+++ b/cmd/cli/run/inrun.go
@@ -15,27 +15,27 @@ import (
)
// runRuntime is the runtime build's root command: bare `inrun` runs the
-// runtime. Without -f it reads catalog.yaml (or stack.yaml) from the current
+// runtime. Without -f it reads blueprint.yaml (or stack.yaml) from the current
// directory.
func runRuntime(cmd *cobra.Command, args []string) error {
- // Resolve catalog paths
+ // Resolve blueprint paths
paths, _ := cmd.Flags().GetStringSlice("file")
- paths, err := cmdutil.ResolveCatalogPaths(paths)
+ paths, err := cmdutil.ResolveBlueprintPaths(paths)
if err != nil {
return err
}
- // Merge catalogs
+ // Merge blueprints
m := merger.New(paths...)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging catalogs: %w", err)
+ return fmt.Errorf("merging blueprints: %w", err)
}
logger.Debug().
- Str("catalogs", strings.Join(paths, ", ")).
+ Str("blueprints", strings.Join(paths, ", ")).
Int("total", m.Count()).
Int("enabled", m.EnabledCount()).
- Msg("catalogs merged")
+ Msg("blueprints merged")
internal.RunRuntime(cmdutil.Cfg, m, cmdutil.Ctx)
return nil
diff --git a/cmd/cli/run/inrun_dev.go b/cmd/cli/run/inrun_dev.go
index 86905002b..99ec7923b 100644
--- a/cmd/cli/run/inrun_dev.go
+++ b/cmd/cli/run/inrun_dev.go
@@ -10,7 +10,7 @@ import (
"github.com/intentd/inrun/cmd/cli/run/companions"
"github.com/intentd/inrun/cmd/internal"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/logger"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/registry"
@@ -18,7 +18,7 @@ import (
"github.com/spf13/cobra"
)
-// runDev is the dev build's root command. Bare `inrun` runs the Catalog in the
+// runDev is the dev build's root command. Bare `inrun` runs the Blueprint in the
// current directory (or -f); `inrun :` pulls a published
// pattern and runs it.
func runDev(cmd *cobra.Command, args []string) error {
@@ -51,23 +51,23 @@ func runDev(cmd *cobra.Command, args []string) error {
paths = append([]string{p}, paths...)
}
- // Resolve catalog paths
- paths, err := cmdutil.ResolveCatalogPaths(paths)
+ // Resolve blueprint paths
+ paths, err := cmdutil.ResolveBlueprintPaths(paths)
if err != nil {
return err
}
- // Merge catalogs
+ // Merge blueprints
m := merger.New(paths...)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging catalogs: %w", err)
+ return fmt.Errorf("merging blueprints: %w", err)
}
logger.Debug().
- Strs("catalogs", paths).
+ Strs("blueprints", paths).
Int("total", m.Count()).
Int("enabled", m.EnabledCount()).
- Msg("catalogs merged")
+ Msg("blueprints merged")
// Apply declared crdFile, crFiles and setup paths before handing off to the runtime.
if len(paths) > 0 {
@@ -77,10 +77,12 @@ func runDev(cmd *cobra.Command, args []string) error {
}
}
- // Run the runtime. Once its Catalog is built, start the console, and the
- // gateway when the Catalog serves.
- internal.RunRuntime(cmdutil.Cfg, m, cmdutil.Ctx, func(cat *catalog.Catalog) {
- companions.Start(cmdutil.Ctx, cmdutil.Cfg, paths, cat)
+ migrate, _ := cmd.Flags().GetBool("migrate")
+
+ // Run the runtime. Once its Blueprint is built, start the console, and the
+ // gateway when the Blueprint serves.
+ internal.RunRuntime(cmdutil.Cfg, m, cmdutil.Ctx, func(bp *blueprint.Blueprint) {
+ companions.Start(cmdutil.Ctx, cmdutil.Cfg, paths, bp, migrate)
})
return nil
}
@@ -113,7 +115,8 @@ func init() {
root.Flags().Bool("dev", false, "Create a local Kind cluster if none is reachable (development only)")
root.Flags().Bool("dev-server", false, "Start the mock dev server for external: examples (no real services needed)")
root.Flags().Int("dev-server-port", devserver.Port, "Port for the mock dev server")
- root.Flags().Bool("use-stack", false, "Run the pattern's stack.yaml instead of its catalog.yaml")
+ root.Flags().Bool("use-stack", false, "Run the pattern's stack.yaml instead of its blueprint.yaml")
root.Flags().Bool("refresh", false, "Pull the pattern again even if it is cached")
root.Flags().Bool("apply-cr", false, "Apply the pattern's example CRD and CR before starting")
+ root.Flags().Bool("migrate", false, "Run schema migration before the gateway starts")
}
diff --git a/cmd/cli/run/inrun_dev_apply.go b/cmd/cli/run/inrun_dev_apply.go
index 37c387a2a..116515847 100644
--- a/cmd/cli/run/inrun_dev_apply.go
+++ b/cmd/cli/run/inrun_dev_apply.go
@@ -26,17 +26,17 @@ import (
// 4. Setup files (setup)
//
// All resources are applied in the correct order before the operator runtime
-// starts. Relative paths are resolved against the catalog directory. This
+// starts. Relative paths are resolved against the blueprint directory. This
// function is a no-op when running inside the cluster.
-func applyPreRuntimeResources(ctx context.Context, catalogPath string, m *merger.Merger) {
+func applyPreRuntimeResources(ctx context.Context, blueprintPath string, m *merger.Merger) {
if cmdutil.IsRunningInPod() {
return
}
- applyCRDFilesIfNeeded(ctx, catalogPath, m)
+ applyCRDFilesIfNeeded(ctx, blueprintPath, m)
waitForCRDsEstablished(ctx, m)
- applyCRFilesIfNeeded(ctx, catalogPath, m)
- applySetupIfNeeded(ctx, catalogPath, m)
+ applyCRFilesIfNeeded(ctx, blueprintPath, m)
+ applySetupIfNeeded(ctx, blueprintPath, m)
}
// ensureClusterReady ensures that a Kubernetes cluster is reachable before
@@ -48,6 +48,9 @@ func ensureClusterReady(dev bool) error {
if err := cluster.EnsureDependencies(); err != nil {
return fmt.Errorf("installing dependencies: %w", err)
}
+ if cluster.ClusterReachable() {
+ return nil
+ }
fmt.Println("\n Cannot reach a Kubernetes cluster.")
fmt.Printf(" Creating local Kind cluster '%s'...\n", cluster.KindClusterName)
@@ -59,7 +62,7 @@ func ensureClusterReady(dev bool) error {
return nil
}
- // non-dev mode
+ // non --dev mode
if cluster.ClusterReachable() {
return nil
}
@@ -96,12 +99,12 @@ func ensureClusterReady(dev bool) error {
// applyCRFilesIfNeeded applies crFiles declarations via kubectl in order before
// the runtime starts. Only runs outside the cluster (dev mode).
-func applyCRFilesIfNeeded(ctx context.Context, catalogPath string, m *merger.Merger) {
+func applyCRFilesIfNeeded(ctx context.Context, blueprintPath string, m *merger.Merger) {
if cmdutil.IsRunningInPod() {
return
}
- catalogDir := filepath.Dir(catalogPath)
+ blueprintDir := filepath.Dir(blueprintPath)
for crdName, entry := range m.All() {
if !entry.HasCRFiles() {
@@ -110,7 +113,7 @@ func applyCRFilesIfNeeded(ctx context.Context, catalogPath string, m *merger.Mer
for _, crFile := range entry.CRFiles {
path := crFile
if !filepath.IsAbs(path) && !strings.HasPrefix(path, "http") {
- path = filepath.Join(catalogDir, path)
+ path = filepath.Join(blueprintDir, path)
}
out, err := exec.CommandContext(ctx, "kubectl", "apply", "-f", path).CombinedOutput()
@@ -175,12 +178,12 @@ func waitForCRDsEstablished(ctx context.Context, m *merger.Merger) {
// applyCRDFilesIfNeeded applies any crdFile declarations via kubectl before the
// operator starts. Only runs outside the cluster (dev mode). In production,
// CRDs must be pre-applied by the platform operator.
-func applyCRDFilesIfNeeded(ctx context.Context, catalogPath string, m *merger.Merger) {
+func applyCRDFilesIfNeeded(ctx context.Context, blueprintPath string, m *merger.Merger) {
if cmdutil.IsRunningInPod() {
return
}
- catalogDir := filepath.Dir(catalogPath)
+ blueprintDir := filepath.Dir(blueprintPath)
for crdName, entry := range m.All() {
if !entry.HasCRDFile() {
@@ -189,7 +192,7 @@ func applyCRDFilesIfNeeded(ctx context.Context, catalogPath string, m *merger.Me
path := entry.CRDFile
if !filepath.IsAbs(path) && !strings.HasPrefix(path, "http") {
- path = filepath.Join(catalogDir, path)
+ path = filepath.Join(blueprintDir, path)
}
out, err := exec.CommandContext(ctx, "kubectl", "apply", "-f", path).CombinedOutput()
@@ -211,10 +214,10 @@ func applyCRDFilesIfNeeded(ctx context.Context, catalogPath string, m *merger.Me
// applyPatternExamples applies crd.yaml and cr.yaml from the pattern directory
// when --apply-cr is set. These are the example files shipped with the pattern —
-// distinct from crdFile/crFiles declared in the catalog itself. kubectl apply
+// distinct from crdFile/crFiles declared in the blueprint itself. kubectl apply
// is idempotent so overlap with applyPreRuntimeResources is safe.
-func applyPatternExamples(ctx context.Context, catalogPath string, m *merger.Merger) {
- dir := filepath.Dir(catalogPath)
+func applyPatternExamples(ctx context.Context, blueprintPath string, m *merger.Merger) {
+ dir := filepath.Dir(blueprintPath)
if rel := registry.FindPatternFile(dir, registry.FileCRD); rel != "" {
crdPath := filepath.Join(dir, rel)
@@ -240,12 +243,12 @@ func applyPatternExamples(ctx context.Context, catalogPath string, m *merger.Mer
// applySetupIfNeeded applies setup YAML files via kubectl in order before
// Inrun starts. Only runs outside the cluster (dev mode).
-func applySetupIfNeeded(ctx context.Context, catalogPath string, m *merger.Merger) {
+func applySetupIfNeeded(ctx context.Context, blueprintPath string, m *merger.Merger) {
if cmdutil.IsRunningInPod() {
return
}
- catalogDir := filepath.Dir(catalogPath)
+ blueprintDir := filepath.Dir(blueprintPath)
for crdName, entry := range m.All() {
if !entry.HasSetup() {
@@ -255,7 +258,7 @@ func applySetupIfNeeded(ctx context.Context, catalogPath string, m *merger.Merge
for _, setupFile := range entry.Setup.Apply {
path := setupFile.Path
if !filepath.IsAbs(path) && !strings.HasPrefix(path, "http") {
- path = filepath.Join(catalogDir, path)
+ path = filepath.Join(blueprintDir, path)
}
out, err := exec.CommandContext(ctx, "kubectl", "apply", "-f", path).CombinedOutput()
diff --git a/cmd/cli/scaffold/create_pattern.go b/cmd/cli/scaffold/create_pattern.go
index bc40aaa59..6a0e67aab 100644
--- a/cmd/cli/scaffold/create_pattern.go
+++ b/cmd/cli/scaffold/create_pattern.go
@@ -14,11 +14,11 @@ import (
var createPatternCmd = &cobra.Command{
Use: "pattern",
- Short: "Scaffold a new Inrun pattern: catalog.yaml, simulate.yaml, e2e.yaml, README.md",
+ Short: "Scaffold a new Inrun pattern: blueprint.yaml, simulate.yaml, e2e.yaml, README.md",
Long: `Creates the files needed to build, test, and publish an Inrun pattern.
Always written:
- catalog.yaml — operator declaration
+ blueprint.yaml — operator declaration
simulate.yaml — in-memory test scaffold (inrun simulate)
e2e.yaml — real-cluster integration test scaffold (inrun e2e)
README.md — actionable steps from edit to release
@@ -28,7 +28,7 @@ Also written when --typed, --add-hook, or --add-constructor:
Makefile — registry, build, build-runtime, docker, push, release
Dockerfile — production container image (distroless, runtime binary only)
-Typed mode flags are forwarded to catalog generation:
+Typed mode flags are forwarded to blueprint generation:
--add-hook Include a hooks section
--add-constructor Include a constructor section
--typed Include both hooks and constructor (commented)
@@ -49,20 +49,20 @@ Examples:
isTyped := typed || addHook || addConstructor
- catalogOpts := generate.CatalogScaffoldOptions{
+ blueprintOpts := generate.BlueprintScaffoldOptions{
AddHook: addHook,
AddConstructor: addConstructor,
Typed: typed,
- OutputFile: filepath.Join(outputDir, cmdutil.FileCatalog),
+ OutputFile: filepath.Join(outputDir, cmdutil.FileBlueprint),
}
- if err := catalogOpts.Validate(); err != nil {
+ if err := blueprintOpts.Validate(); err != nil {
return err
}
fmt.Printf("generating pattern scaffold → %s/\n", outputDir)
- if _, err := generate.CatalogScaffold(catalogOpts); err != nil {
- return fmt.Errorf("generating %s: %w", cmdutil.FileCatalog, err)
+ if _, err := generate.BlueprintScaffold(blueprintOpts); err != nil {
+ return fmt.Errorf("generating %s: %w", cmdutil.FileBlueprint, err)
}
if err := generate.WriteSimulateScaffold(filepath.Join(outputDir, cmdutil.DirTest, cmdutil.FileSimulate)); err != nil {
@@ -90,7 +90,7 @@ Examples:
}
fmt.Printf("\n→ pattern scaffold written to %s\n", cmdutil.Bold(outputDir+"/"))
- fmt.Printf(" %s %-16s %s\n", cmdutil.SuccessMark(), cmdutil.FileCatalog, cmdutil.Dim("declare your CRD(s) and resources"))
+ fmt.Printf(" %s %-16s %s\n", cmdutil.SuccessMark(), cmdutil.FileBlueprint, cmdutil.Dim("declare your CRD(s) and resources"))
fmt.Printf(" %s %-16s %s\n", cmdutil.SuccessMark(), cmdutil.DirTest+"/"+cmdutil.FileSimulate, cmdutil.Dim("inrun simulate"))
fmt.Printf(" %s %-16s %s\n", cmdutil.SuccessMark(), cmdutil.DirTest+"/"+cmdutil.FileE2e, cmdutil.Dim("inrun e2e"))
fmt.Printf(" %s %-16s %s\n", cmdutil.SuccessMark(), cmdutil.FileReadMe, cmdutil.Dim("start here"))
@@ -107,9 +107,9 @@ Examples:
func init() {
cmdutil.CreateCmd.AddCommand(createPatternCmd)
createPatternCmd.Flags().Bool("add-hook", false,
- "Typed mode: include a hooks section in catalog.yaml (also writes Makefile + Dockerfile)")
+ "Typed mode: include a hooks section in blueprint.yaml (also writes Makefile + Dockerfile)")
createPatternCmd.Flags().Bool("add-constructor", false,
- "Typed mode: include a constructor section in catalog.yaml (also writes Makefile + Dockerfile)")
+ "Typed mode: include a constructor section in blueprint.yaml (also writes Makefile + Dockerfile)")
createPatternCmd.Flags().Bool("typed", false,
"Typed mode: include both hooks and constructor commented (also writes Makefile + Dockerfile)")
createPatternCmd.Flags().StringP("output", "o", "",
diff --git a/cmd/cli/scaffold/init.go b/cmd/cli/scaffold/init.go
index 47cf1fcae..35d055855 100644
--- a/cmd/cli/scaffold/init.go
+++ b/cmd/cli/scaffold/init.go
@@ -68,7 +68,7 @@ func initCanonical(name string) error {
if !isCurrentDirectory(name) {
steps = append(steps, initStep{"Creating project folder", func() error { return os.MkdirAll(name, 0755) }})
}
- steps = append(steps, initStep{"Writing catalog.yaml", func() error { return extractCanonical(name) }})
+ steps = append(steps, initStep{"Writing blueprint.yaml", func() error { return extractCanonical(name) }})
if err := runSteps(steps); err != nil {
return err
diff --git a/cmd/cli/scaffold/init_helper.go b/cmd/cli/scaffold/init_helper.go
index 878bbabe9..ce3be92f7 100644
--- a/cmd/cli/scaffold/init_helper.go
+++ b/cmd/cli/scaffold/init_helper.go
@@ -95,12 +95,12 @@ func extractEmbeddedPack(root, pack string) error {
}
// extractCanonical writes the canonical hello-website operator into the
-// project root in the standard layout: catalog.yaml, manifests/ (CRD and CR)
+// project root in the standard layout: blueprint.yaml, manifests/ (CRD and CR)
// and test/ (simulate and e2e specs).
func extractCanonical(root string) error {
srcPath := "reconcilers/declarative/01-hello-website"
keep := map[string]bool{
- cmdutil.FileCatalog: true,
+ cmdutil.FileBlueprint: true,
filepath.Join(cmdutil.DirManifests, cmdutil.FileCrd): true,
filepath.Join(cmdutil.DirManifests, cmdutil.FileCr): true,
filepath.Join(cmdutil.DirTest, cmdutil.FileSimulate): true,
diff --git a/cmd/cli/scaffold/init_packs.go b/cmd/cli/scaffold/init_packs.go
index 562263c79..eaccaa439 100644
--- a/cmd/cli/scaffold/init_packs.go
+++ b/cmd/cli/scaffold/init_packs.go
@@ -21,7 +21,7 @@ type Pack struct {
var Packs = map[string]Pack{
"declarative": {
Name: "declarative",
- Description: "Start here. Operators declared in a Catalog, no code.",
+ Description: "Start here. Operators declared in a Blueprint, no code.",
Path: "reconcilers/declarative",
First: "01-hello-website",
Order: 1,
@@ -30,6 +30,7 @@ var Packs = map[string]Pack{
Name: "remote",
Description: "A reconciler in any language, called over HTTP.",
Path: "reconcilers/remote",
+ First: "endpoint",
Order: 2,
},
"typed": {
@@ -45,6 +46,20 @@ var Packs = map[string]Pack{
Path: "intent",
Order: 4,
},
+ "migration": {
+ Name: "migration",
+ Description: "Change the CRD under live CRs: move fields, or move CRs to another CRD.",
+ Path: "migration",
+ First: "field/relocation",
+ Order: 5,
+ },
+ "registry": {
+ Name: "registry",
+ Description: "Share operators as patterns: consume one, publish your own, deprecate it.",
+ Path: "registry",
+ First: "consume",
+ Order: 6,
+ },
}
func GetPack(name string) (Pack, bool) {
diff --git a/cmd/cli/serve/chain.go b/cmd/cli/serve/chain.go
index 5019e5939..af712d475 100644
--- a/cmd/cli/serve/chain.go
+++ b/cmd/cli/serve/chain.go
@@ -14,8 +14,8 @@ import (
"github.com/intentd/inrun/cmd/cli/gate"
"github.com/intentd/inrun/cmd/cli/testsuite"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
"github.com/intentd/inrun/pkg/gateway/api"
"github.com/intentd/inrun/pkg/intent"
"github.com/intentd/inrun/pkg/merger"
@@ -29,7 +29,7 @@ import (
// planChain runs stages 1-6 of the play chain against an already-resolved
// intent map, reporting each stage through n: stageNarrator for serve play,
// quietNarrator for simulate. The steps are the same either way.
-func planChain(c *catalog.Catalog, raw map[string]interface{}, tokenName string, op types.ServeOperation, n narrator) (*unstructured.Unstructured, *types.CRDEntry, string, error) {
+func planChain(c *blueprint.Blueprint, raw map[string]interface{}, tokenName string, op types.ServeOperation, n narrator) (*unstructured.Unstructured, *types.CRDEntry, string, error) {
target, _ := raw["target"].(string)
if strings.TrimSpace(target) == "" {
return nil, nil, "", fmt.Errorf(`"target" must be set in the intent`)
@@ -56,8 +56,8 @@ func planChain(c *catalog.Catalog, raw map[string]interface{}, tokenName string,
// Stage 3: CR construction
n.stage(3, "CR construction")
- notes := c.Notes
- obj, err := intent.Build(raw, crd, notes)
+ funcs := c.Functions
+ obj, err := intent.BuildCR(raw, crd, funcs)
if err != nil {
n.fail(err.Error())
return nil, nil, "", err
@@ -91,7 +91,7 @@ func planChain(c *catalog.Catalog, raw map[string]interface{}, tokenName string,
// unique: operator is skipped (no live cluster); external: calls are skipped.
n.stage(5, "Admission validation")
{
- resolver := template.NewResolverFromMap(obj.Object).WithUserNotes(c.Notes).WithRequest(raw)
+ resolver := template.NewResolverFromMap(obj.Object).WithUserFunctions(c.Functions).WithIntent(raw)
eval := resolver.TemplateEvaluator()
r := gate.EvalAdmissionValidation(obj.Object, crd, resolver, eval)
for _, v := range r.Violations {
@@ -129,8 +129,8 @@ func planChain(c *catalog.Catalog, raw map[string]interface{}, tokenName string,
// Stage 6: Response payload
n.stage(6, "Response payload")
- if payload := api.EvaluatePayload(obj.Object, crd, alias, notes); payload == nil {
- n.detail(cmdutil.Gray("no serve.config.response declared — default CR response"))
+ if payload := api.EvaluatePayload(obj.Object, crd, alias, funcs); payload == nil {
+ n.detail(cmdutil.Gray("no serve.view declared — the full CR is returned"))
} else {
n.json(payload)
}
@@ -141,9 +141,9 @@ func planChain(c *catalog.Catalog, raw map[string]interface{}, tokenName string,
// playRunSimulate writes the built CR to a temp file and hands it to simulate.
// simulateConfig is the path to a simulate.yaml; empty means op-print mode with
-// the same catalog play used. Shared by "inrun serve play --simulate" and
+// the same blueprint play used. Shared by "inrun serve play --simulate" and
// "inrun webhook play --simulate" via playSimulate below.
-func playRunSimulate(ctx context.Context, catalogFile string, obj *unstructured.Unstructured, simulateConfig string, simOpts testsuite.CliSimulateOptions) error {
+func playRunSimulate(ctx context.Context, blueprintFile string, obj *unstructured.Unstructured, simulateConfig string, simOpts testsuite.CliSimulateOptions) error {
b, err := yaml.Marshal(obj.Object)
if err != nil {
return fmt.Errorf("marshalling CR for simulate: %w", err)
@@ -167,10 +167,10 @@ func playRunSimulate(ctx context.Context, catalogFile string, obj *unstructured.
}
simOpts.MaxCycles = 10
simOpts.SkipExternal = true
- return testsuite.RunSimulate(ctx, catalogFile, tmp.Name(), simOpts)
+ return testsuite.RunSimulate(ctx, blueprintFile, tmp.Name(), simOpts)
}
-// runSimulateWithCR runs simulate using the spec file for catalog/cycles/expect
+// runSimulateWithCR runs simulate using the spec file for blueprint/cycles/expect
// but substitutes crFile (the play-built CR) instead of spec.cr.
// The expect: block is fully evaluated — this is assert mode, not op-print mode.
func runSimulateWithCR(ctx context.Context, specPath, crFile string, simOpts testsuite.CliSimulateOptions) error {
@@ -189,28 +189,28 @@ func runSimulateWithCR(ctx context.Context, specPath, crFile string, simOpts tes
return fmt.Errorf("simulate config %q: missing spec", specPath)
}
- catalogPath := cmdutil.JoinPath(specPath, doc.Spec.Catalog)
+ blueprintPath := cmdutil.JoinPath(specPath, doc.Spec.Blueprint)
cycles := doc.Spec.Cycles
if cycles <= 0 {
cycles = 10
}
opts := simulate.RunOptions{SkipExternal: doc.Spec.SkipExternal}
- m := merger.New(catalogPath)
+ m := merger.New(blueprintPath)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging Catalog: %w", err)
+ return fmt.Errorf("merging Blueprint: %w", err)
}
- // --envtest installs the spec's CRDs, else the ones the Catalog points at.
+ // --envtest installs the spec's CRDs, else the ones the Blueprint points at.
var crdPaths []string
for _, p := range doc.Spec.AllCRDPaths() {
crdPaths = append(crdPaths, cmdutil.JoinPath(specPath, p))
}
if len(crdPaths) == 0 {
- crdPaths = testsuite.CatalogCRDPaths(m)
+ crdPaths = testsuite.BlueprintCRDPaths(m)
}
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
if err != nil {
- return fmt.Errorf("parsing Catalog: %w", err)
+ return fmt.Errorf("parsing Blueprint: %w", err)
}
crData, err := cmdutil.ReadLocal(crFile)
@@ -223,14 +223,14 @@ func runSimulateWithCR(ctx context.Context, specPath, crFile string, simOpts tes
}
var failed []string
- for _, name := range cat.CRDNames() {
- crdEntry, ok := cat.CRDEntry(name)
+ for _, name := range bp.CRDNames() {
+ crdEntry, ok := bp.CRDEntry(name)
if !ok {
continue
}
in, ok := cmdutil.ResolveCRInputs(crs, crdEntry.APITypes.Kind)
if !ok {
- if len(cat.CRDNames()) > 1 {
+ if len(bp.CRDNames()) > 1 {
fmt.Printf(" %s no CR found for %s — skipped\n\n", cmdutil.Dim("note:"), crdEntry.APITypes.Kind)
continue
}
@@ -240,7 +240,7 @@ func runSimulateWithCR(ctx context.Context, specPath, crFile string, simOpts tes
crdOpts.Peers = in.Peers
crdOpts.ExistingInstances = in.Existing
expect := simulate.ExpectForCRD(doc.Spec.Expect, name)
- if err := testsuite.SimulateOne(ctx, cat, name, in.CR, cycles, crdOpts, simOpts, crdPaths, expect); err != nil {
+ if err := testsuite.SimulateOne(ctx, bp, name, in.CR, cycles, crdOpts, simOpts, crdPaths, expect); err != nil {
failed = append(failed, name)
}
}
@@ -250,17 +250,17 @@ func runSimulateWithCR(ctx context.Context, specPath, crFile string, simOpts tes
return nil
}
-// RunIntentPlay builds a Catalog from catalogPath, runs the play chain
+// RunIntentPlay builds a Blueprint from blueprintPath, runs the play chain
// against intentFile without printing stage output, and returns the resolved
// target name on success.
-func RunIntentPlay(catalogPath, intentFile string) (string, error) {
- m := merger.New(catalogPath)
+func RunIntentPlay(blueprintPath, intentFile string) (string, error) {
+ m := merger.New(blueprintPath)
if err := m.Merge(); err != nil {
- return "", fmt.Errorf("merging catalog: %w", err)
+ return "", fmt.Errorf("merging blueprint: %w", err)
}
c, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
if err != nil {
- return "", fmt.Errorf("building catalog: %w", err)
+ return "", fmt.Errorf("building blueprint: %w", err)
}
raw, err := readIntentFile(intentFile)
@@ -288,12 +288,12 @@ func RunIntentPlay(catalogPath, intentFile string) (string, error) {
return target, fmt.Errorf("intent file must declare a 'token' — token: ")
}
- obj, err := intent.Build(raw, crd, c.Notes)
+ obj, err := intent.BuildCR(raw, crd, c.Functions)
if err != nil {
return target, fmt.Errorf("CR construction: %w", err)
}
- resolver := template.NewResolverFromMap(obj.Object).WithUserNotes(c.Notes).WithRequest(raw)
+ resolver := template.NewResolverFromMap(obj.Object).WithUserFunctions(c.Functions).WithIntent(raw)
eval := resolver.TemplateEvaluator()
r := gate.EvalAdmissionValidation(obj.Object, crd, resolver, eval)
denied := r.Denied()
@@ -316,13 +316,13 @@ func init() {
return nil, fmt.Errorf("reading intent file %q: %w", req.IntentFile, err)
}
target, _ := raw["target"].(string)
- printPlayHeaderForSimulate(req.CatalogFile, req.IntentFile, target, req.Token, req.Cycles)
+ printPlayHeaderForSimulate(req.BlueprintFile, req.IntentFile, target, req.Token, req.Cycles)
var n narrator = quietNarrator{}
if req.Verbose {
n = stageNarrator{}
}
- obj, _, _, err := planChain(req.Catalog, raw, req.Token, req.Op, n)
+ obj, _, _, err := planChain(req.Blueprint, raw, req.Token, req.Op, n)
if err == nil && !req.Verbose {
printPlayed(req.IntentFile, obj)
}
diff --git a/cmd/cli/serve/chain_test.go b/cmd/cli/serve/chain_test.go
index f07bde260..6fe1469fc 100644
--- a/cmd/cli/serve/chain_test.go
+++ b/cmd/cli/serve/chain_test.go
@@ -5,15 +5,15 @@ package serve
import (
"testing"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/types"
)
-// chainTestCatalog builds a single-CRD catalog for kind "ServiceRequest" —
+// chainTestBlueprint builds a single-CRD blueprint for kind "ServiceRequest" —
// its ServeTarget() defaults to the lowercased kind, "servicerequest",
// which every test below uses directly.
-func chainTestCatalog(serveName string, tokens map[string]types.ServeTokenPermissions) *catalog.Catalog {
- cat := catalog.NewFromEntryPointers(map[string]*types.CRDEntry{
+func chainTestBlueprint(serveName string, tokens map[string]types.ServeTokenPermissions) *blueprint.Blueprint {
+ bp := blueprint.NewFromEntryPointers(map[string]*types.CRDEntry{
"servicerequest": {
APITypes: types.APITypes{
Group: "demo.inrun.dev",
@@ -30,26 +30,26 @@ func chainTestCatalog(serveName string, tokens map[string]types.ServeTokenPermis
},
})
// Every token a test lists in serve.tokens is also a gateway token, as in a
- // real Catalog; "dev" is always declared.
+ // real Blueprint; "dev" is always declared.
declared := []types.APIToken{{Name: "dev", Token: "dev-token"}}
for name := range tokens {
if name != "dev" {
declared = append(declared, types.APIToken{Name: name, Token: name + "-token"})
}
}
- cat.Gateway = &types.GatewayConfig{Enabled: true, API: &types.GatewayAPIConfig{
+ bp.Gateway = &types.GatewayConfig{Enabled: true, API: &types.GatewayAPIConfig{
Enabled: true,
Auth: types.APIAuth{Tokens: declared},
}}
- return cat
+ return bp
}
func TestPlayChain_UndeclaredTokenDenied(t *testing.T) {
// No serve.tokens on the target: any token declared in
// gateway.api.auth.tokens may create, but an undeclared name may not.
- cat := chainTestCatalog("", nil)
+ bp := chainTestBlueprint("", nil)
_, _, _, err := planChain(
- cat, map[string]interface{}{"target": "servicerequest", "name": "x"}, "unknown", types.ServeOpCreate, quietNarrator{},
+ bp, map[string]interface{}{"target": "servicerequest", "name": "x"}, "unknown", types.ServeOpCreate, quietNarrator{},
)
if err == nil {
t.Fatal("expected a token missing from gateway.api.auth.tokens to be denied")
@@ -57,9 +57,9 @@ func TestPlayChain_UndeclaredTokenDenied(t *testing.T) {
}
func TestPlayChain_UnknownTarget(t *testing.T) {
- cat := chainTestCatalog("", nil)
+ bp := chainTestBlueprint("", nil)
_, _, _, err := planChain(
- cat, map[string]interface{}{"target": "does-not-exist"}, "dev", types.ServeOpCreate, quietNarrator{},
+ bp, map[string]interface{}{"target": "does-not-exist"}, "dev", types.ServeOpCreate, quietNarrator{},
)
if err == nil {
t.Fatal("expected an error for an unknown target")
@@ -67,9 +67,9 @@ func TestPlayChain_UnknownTarget(t *testing.T) {
}
func TestPlayChain_MissingTarget(t *testing.T) {
- cat := chainTestCatalog("", nil)
+ bp := chainTestBlueprint("", nil)
_, _, _, err := planChain(
- cat, map[string]interface{}{"name": "x"}, "dev", types.ServeOpCreate, quietNarrator{},
+ bp, map[string]interface{}{"name": "x"}, "dev", types.ServeOpCreate, quietNarrator{},
)
if err == nil {
t.Fatal(`expected an error when "target" is absent`)
@@ -77,11 +77,11 @@ func TestPlayChain_MissingTarget(t *testing.T) {
}
func TestPlayChain_TokenDenied(t *testing.T) {
- cat := chainTestCatalog("", map[string]types.ServeTokenPermissions{
+ bp := chainTestBlueprint("", map[string]types.ServeTokenPermissions{
"dev": {Permissions: types.ServePermissionSet{Global: []string{"get"}}}, // no create
})
_, _, _, err := planChain(
- cat, map[string]interface{}{"target": "servicerequest", "name": "x"}, "dev", types.ServeOpCreate, quietNarrator{},
+ bp, map[string]interface{}{"target": "servicerequest", "name": "x"}, "dev", types.ServeOpCreate, quietNarrator{},
)
if err == nil {
t.Fatal("expected the token to be denied create")
@@ -91,11 +91,11 @@ func TestPlayChain_TokenDenied(t *testing.T) {
func TestPlayChain_UnknownTokenDenied(t *testing.T) {
// Once ANY token is declared, an unlisted token name is denied outright —
// no implicit fallback to "allow all".
- cat := chainTestCatalog("", map[string]types.ServeTokenPermissions{
+ bp := chainTestBlueprint("", map[string]types.ServeTokenPermissions{
"dev": {Permissions: types.ServePermissionSet{Global: []string{"*"}}},
})
_, _, _, err := planChain(
- cat, map[string]interface{}{"target": "servicerequest", "name": "x"}, "someone-else", types.ServeOpCreate, quietNarrator{},
+ bp, map[string]interface{}{"target": "servicerequest", "name": "x"}, "someone-else", types.ServeOpCreate, quietNarrator{},
)
if err == nil {
t.Fatal("expected an unlisted token name to be denied")
@@ -103,9 +103,9 @@ func TestPlayChain_UnknownTokenDenied(t *testing.T) {
}
func TestPlayChain_BuildsCRWithProvenance(t *testing.T) {
- cat := chainTestCatalog("", nil) // no serve.tokens: any declared gateway token may create
+ bp := chainTestBlueprint("", nil) // no serve.tokens: any declared gateway token may create
obj, crd, alias, err := planChain(
- cat,
+ bp,
map[string]interface{}{"target": "servicerequest", "name": "payments-api"},
"dev", types.ServeOpCreate, quietNarrator{},
)
@@ -128,9 +128,9 @@ func TestPlayChain_BuildsCRWithProvenance(t *testing.T) {
func TestPlayChain_MissingNameRejected(t *testing.T) {
// serve.name isn't declared and no raw "name" is supplied.
- cat := chainTestCatalog("", nil)
+ bp := chainTestBlueprint("", nil)
_, _, _, err := planChain(
- cat, map[string]interface{}{"target": "servicerequest"}, "dev", types.ServeOpCreate, quietNarrator{},
+ bp, map[string]interface{}{"target": "servicerequest"}, "dev", types.ServeOpCreate, quietNarrator{},
)
if err == nil {
t.Fatal("expected an error when neither serve.name nor a raw name is available")
diff --git a/cmd/cli/serve/helper.go b/cmd/cli/serve/helper.go
index 7d62ca151..0fcf9b727 100644
--- a/cmd/cli/serve/helper.go
+++ b/cmd/cli/serve/helper.go
@@ -13,7 +13,7 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/intent"
"github.com/intentd/inrun/pkg/types"
)
@@ -58,22 +58,22 @@ func aliasOrNone(alias string) string {
}
// resolveCRD resolves a CRD by target, kind, or name.
-func resolveCRD(cat *catalog.Catalog, target, kind, name string) (*types.CRDEntry, error) {
+func resolveCRD(bp *blueprint.Blueprint, target, kind, name string) (*types.CRDEntry, error) {
var crd *types.CRDEntry
switch {
case target != "":
- crd = cat.LookupByTarget(target).Entry()
+ crd = bp.LookupByTarget(target).Entry()
if crd == nil {
return nil, fmt.Errorf("%s target %q not found", cmdutil.FailureMark(), target)
}
case kind != "":
- crd = cat.LookupByKind(kind).Entry()
+ crd = bp.LookupByKind(kind).Entry()
if crd == nil {
return nil, fmt.Errorf("%s kind %q not found", cmdutil.FailureMark(), kind)
}
case name != "":
- crd = cat.LookupByName(name).Entry()
+ crd = bp.LookupByName(name).Entry()
if crd == nil {
return nil, fmt.Errorf("%s CRD %q not found", cmdutil.FailureMark(), name)
}
@@ -87,8 +87,8 @@ func resolveCRD(cat *catalog.Catalog, target, kind, name string) (*types.CRDEntr
// resolveCRDByAnyTarget resolves a CRD by primary target or alias using
// LookupByTargetOrAlias. Returns the CRD and the alias name (empty when the
// primary target matched). Used by can-i so --target accepts alias names too.
-func resolveCRDByAnyTarget(cat *catalog.Catalog, target string) (*types.CRDEntry, string, error) {
- resolution := cat.LookupByTargetOrAlias(target)
+func resolveCRDByAnyTarget(bp *blueprint.Blueprint, target string) (*types.CRDEntry, string, error) {
+ resolution := bp.LookupByTargetOrAlias(target)
if resolution == nil {
return nil, "", fmt.Errorf("%s target or alias %q not found", cmdutil.FailureMark(), target)
}
@@ -99,7 +99,7 @@ func resolveCRDByAnyTarget(cat *catalog.Catalog, target string) (*types.CRDEntry
// declared in gateway.api.auth.tokens (the gateway authenticates nothing
// else), and the target's serve.tokens, when set, must allow op.
// returns error when token is not declared or denied
-func checkToken(c *catalog.Catalog, crd *types.CRDEntry, alias, tokenName string, op types.ServeOperation, ns string) error {
+func checkToken(c *blueprint.Blueprint, crd *types.CRDEntry, alias, tokenName string, op types.ServeOperation, ns string) error {
if declared := c.GatewayTokenNames(); !slices.Contains(declared, tokenName) {
return fmt.Errorf("unknown token %q\n declared in gateway.api.auth.tokens: %s", tokenName, strings.Join(declared, ", "))
}
@@ -231,7 +231,7 @@ func printAliasesForCRD(crd *types.CRDEntry) {
hasTokens = "yes"
}
hasResponse := "no"
- if cfg != nil && cfg.ResponseConfig() != nil {
+ if cfg != nil && cfg.GetView() != nil {
hasResponse = "yes"
}
fmt.Fprintf(w, " %s\t%s\t%s\n", name, hasTokens, hasResponse)
@@ -242,8 +242,8 @@ func printAliasesForCRD(crd *types.CRDEntry) {
// ── printServeValidationSummary ──────────────────────────────────────────────
// printServeValidationSummary prints a detailed breakdown of the Serve configuration.
-func printServeValidationSummary(cat *catalog.Catalog) {
- enabledCRDs := cat.ServeEnabledCRDs()
+func printServeValidationSummary(bp *blueprint.Blueprint) {
+ enabledCRDs := bp.ServeEnabledCRDs()
if len(enabledCRDs) == 0 {
fmt.Println("\nNo Serve-enabled CRDs found.")
@@ -346,23 +346,23 @@ func printServeValidationSummary(cat *catalog.Catalog) {
} else {
parts = append(parts, "inherits CRD tokens")
}
- if alias != nil && alias.ResponseConfig() != nil {
- parts = append(parts, "custom response")
+ if alias != nil && alias.GetView() != nil {
+ parts = append(parts, "custom view")
}
fmt.Printf(" %s %s %s\n", cmdutil.Gray("·"), cmdutil.Gray(aliasName), cmdutil.Gray("("+strings.Join(parts, ", ")+")"))
}
}
// ── Response Config ─────────────────────────────────────────────
- cfg := crd.GetServeResponseConfig()
+ cfg := crd.ServeView()
if cfg != nil {
payloadCount := len(cfg.Payload)
excludeCount := len(cfg.Exclude)
defaultVal := "true"
- if !cfg.UseDefault() {
+ if !cfg.IncludesFullObject() {
defaultVal = "false"
}
- fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("response: default: %s, payload: %d, exclude: %d", defaultVal, payloadCount, excludeCount)))
+ fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("view: fullObject: %s, payload: %d, exclude: %d", defaultVal, payloadCount, excludeCount)))
if cfg.HasPoll() {
pollParts := []string{}
if cfg.Poll.URL != "" {
@@ -374,7 +374,7 @@ func printServeValidationSummary(cat *catalog.Catalog) {
fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("poll: %s", strings.Join(pollParts, ", "))))
}
} else {
- fmt.Printf(" %s\n", cmdutil.Gray("response: none (default CR response)"))
+ fmt.Printf(" %s\n", cmdutil.Gray("view: none (the full CR is returned)"))
}
// ── Warnings ─────────────────────────────────────────────────────
diff --git a/cmd/cli/serve/play.go b/cmd/cli/serve/play.go
index f1d44e363..5cc593b7e 100644
--- a/cmd/cli/serve/play.go
+++ b/cmd/cli/serve/play.go
@@ -11,7 +11,7 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/types"
"github.com/spf13/cobra"
)
@@ -38,15 +38,15 @@ accepts either as a request body too.
No cluster connection is required. No CR is applied or fetched.
Stages (create/update):
- 1. Target resolution — resolve target/alias from catalog
+ 1. Target resolution — resolve target/alias from blueprint
2. Token check — verify the named token can perform the operation
3. CR construction — build the full CR from field declarations
4. Provenance — stamp serve-target and serve-alias annotations
5. Admission validation — evaluate validation.rules (deny/warn) against the built CR
- 6. Response payload — evaluate serve.config.response.payload expressions
+ 6. Response payload — evaluate serve.view.payload expressions
Stages (get/list/delete):
- 1. Target resolution — resolve target/alias from catalog
+ 1. Target resolution — resolve target/alias from blueprint
2. Token check — verify the named token can perform the operation
3. Response config — show what the caller would receive
@@ -91,7 +91,7 @@ Example intent.json:
return fmt.Errorf("%s --token is required", cmdutil.FailureMark())
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -103,26 +103,26 @@ Example intent.json:
}
if op == types.ServeOpCreate || op == types.ServeOpUpdate {
- // Resolve first catalog path for simulate handoff (--file is a StringSlice on serve commands)
- catalogFile := ""
+ // Resolve first blueprint path for simulate handoff (--file is a StringSlice on serve commands)
+ blueprintFile := ""
paths, _ := cmd.Flags().GetStringSlice("file")
if len(paths) == 0 {
paths = cmdutil.DefaultFilePaths()
}
if len(paths) == 0 {
- return fmt.Errorf(cmdutil.ErrNoCatalog)
+ return fmt.Errorf(cmdutil.ErrNoBlueprint)
}
- catalogFile = paths[0]
+ blueprintFile = paths[0]
- return playWrite(cmd.Context(), cat, catalogFile, intentFile, targetOverride, tokenName, op, simulateFlag, simulateConfig, simOpts)
+ return playWrite(cmd.Context(), bp, blueprintFile, intentFile, targetOverride, tokenName, op, simulateFlag, simulateConfig, simOpts)
}
- return playRead(cat, targetOverride, tokenName, op, namespace, name)
+ return playRead(bp, targetOverride, tokenName, op, namespace, name)
},
}
// ── create / update path ─────────────────────────────────────────────────────
-func playWrite(ctx context.Context, cat *catalog.Catalog, catalogFile, intentFile, targetOverride, tokenName string, op types.ServeOperation, doSimulate bool, simulateConfig string, simOpts testsuite.CliSimulateOptions) error {
+func playWrite(ctx context.Context, bp *blueprint.Blueprint, blueprintFile, intentFile, targetOverride, tokenName string, op types.ServeOperation, doSimulate bool, simulateConfig string, simOpts testsuite.CliSimulateOptions) error {
if intentFile == "" {
intentFile = resolveDefaultIntentFile()
if intentFile == "" {
@@ -145,7 +145,7 @@ func playWrite(ctx context.Context, cat *catalog.Catalog, catalogFile, intentFil
printPlayHeader(intentFile, target, tokenName, op)
- obj, crd, alias, err := planChain(cat, raw, tokenName, op, stageNarrator{})
+ obj, crd, alias, err := planChain(bp, raw, tokenName, op, stageNarrator{})
if err != nil {
return err
}
@@ -159,14 +159,14 @@ func playWrite(ctx context.Context, cat *catalog.Catalog, catalogFile, intentFil
fmt.Println()
if doSimulate {
- return playRunSimulate(ctx, catalogFile, obj, simulateConfig, simOpts)
+ return playRunSimulate(ctx, blueprintFile, obj, simulateConfig, simOpts)
}
return nil
}
// ── get / list / delete path ─────────────────────────────────────────────────
-func playRead(cat *catalog.Catalog, target, tokenName string, op types.ServeOperation, namespace, name string) error {
+func playRead(bp *blueprint.Blueprint, target, tokenName string, op types.ServeOperation, namespace, name string) error {
if strings.TrimSpace(target) == "" {
return fmt.Errorf("%s --target is required for %s", cmdutil.FailureMark(), op)
}
@@ -178,9 +178,9 @@ func playRead(cat *catalog.Catalog, target, tokenName string, op types.ServeOper
// Stage 1: Target resolution
printStage(1, "Target resolution")
- resolution := cat.LookupByTargetOrAlias(target)
+ resolution := bp.LookupByTargetOrAlias(target)
if resolution == nil {
- printStageError(fmt.Sprintf("unknown target %q\n available: %s", target, strings.Join(cat.AvailableTargets(), ", ")))
+ printStageError(fmt.Sprintf("unknown target %q\n available: %s", target, strings.Join(bp.AvailableTargets(), ", ")))
return fmt.Errorf("target %q not found", target)
}
crd, alias := resolution.CRD, resolution.Alias
@@ -188,7 +188,7 @@ func playRead(cat *catalog.Catalog, target, tokenName string, op types.ServeOper
// Stage 2: Token check
printStage(2, "Token check")
- if err := checkToken(cat, crd, alias, tokenName, op, namespace); err != nil {
+ if err := checkToken(bp, crd, alias, tokenName, op, namespace); err != nil {
printStageError(err.Error())
return err
}
@@ -200,14 +200,14 @@ func playRead(cat *catalog.Catalog, target, tokenName string, op types.ServeOper
printStageDetail(cmdutil.Gray(fmt.Sprintf("DELETE %s/%s in %s", crd.Kind(), name, namespaceOrAny(namespace))))
printStageOK("would be deleted")
} else {
- cfg := crd.ServeResponseConfigFor(alias)
+ cfg := crd.ServeViewFor(alias)
if cfg == nil {
- printStageDetail(cmdutil.Gray("no serve.config.response declared — full CR returned"))
+ printStageDetail(cmdutil.Gray("no serve.view declared — full CR returned"))
} else {
- if !cfg.UseDefault() {
- printStageDetail(cmdutil.Gray("default: false — raw CR omitted from response"))
+ if !cfg.IncludesFullObject() {
+ printStageDetail(cmdutil.Gray("fullObject: false — the CR is left out of the view"))
} else {
- printStageDetail(cmdutil.Gray("default: true — full CR included in response"))
+ printStageDetail(cmdutil.Gray("fullObject: true — the full CR is included in the view"))
}
if cfg.HasPayload() {
printStageDetail(cmdutil.Gray(fmt.Sprintf("payload fields: %s", strings.Join(cmdutil.SortedKeys(cfg.Payload), ", "))))
@@ -217,9 +217,9 @@ func playRead(cat *catalog.Catalog, target, tokenName string, op types.ServeOper
}
}
if op == types.ServeOpGet {
- printStageOK(fmt.Sprintf("GET %s/%s in %s — response config above applies", crd.Kind(), name, namespaceOrAny(namespace)))
+ printStageOK(fmt.Sprintf("GET %s/%s in %s — the view above applies", crd.Kind(), name, namespaceOrAny(namespace)))
} else {
- printStageOK(fmt.Sprintf("LIST %s in %s — response config applies to all items", crd.Kind(), namespaceOrAny(namespace)))
+ printStageOK(fmt.Sprintf("LIST %s in %s — the view applies to all items", crd.Kind(), namespaceOrAny(namespace)))
}
}
diff --git a/cmd/cli/serve/play_test.go b/cmd/cli/serve/play_test.go
index f9855701e..1e1407946 100644
--- a/cmd/cli/serve/play_test.go
+++ b/cmd/cli/serve/play_test.go
@@ -116,52 +116,52 @@ func TestPayloadKeys_SortsAlphabetically(t *testing.T) {
}
func TestPlayRead_UnknownTarget(t *testing.T) {
- cat := chainTestCatalog("", nil)
- if err := playRead(cat, "does-not-exist", "dev", types.ServeOpGet, "default", "x"); err == nil {
+ bp := chainTestBlueprint("", nil)
+ if err := playRead(bp, "does-not-exist", "dev", types.ServeOpGet, "default", "x"); err == nil {
t.Fatal("expected an error for an unknown target")
}
}
func TestPlayRead_MissingTargetFlag(t *testing.T) {
- cat := chainTestCatalog("", nil)
- if err := playRead(cat, "", "dev", types.ServeOpGet, "default", "x"); err == nil {
+ bp := chainTestBlueprint("", nil)
+ if err := playRead(bp, "", "dev", types.ServeOpGet, "default", "x"); err == nil {
t.Fatal("expected an error when --target is empty")
}
}
func TestPlayRead_MissingNameForGet(t *testing.T) {
- cat := chainTestCatalog("", nil)
- if err := playRead(cat, "servicerequest", "dev", types.ServeOpGet, "default", ""); err == nil {
+ bp := chainTestBlueprint("", nil)
+ if err := playRead(bp, "servicerequest", "dev", types.ServeOpGet, "default", ""); err == nil {
t.Fatal("expected an error when --name is empty for get")
}
}
func TestPlayRead_ListDoesNotRequireName(t *testing.T) {
- cat := chainTestCatalog("", nil)
- if err := playRead(cat, "servicerequest", "dev", types.ServeOpList, "default", ""); err != nil {
+ bp := chainTestBlueprint("", nil)
+ if err := playRead(bp, "servicerequest", "dev", types.ServeOpList, "default", ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPlayRead_TokenDenied(t *testing.T) {
- cat := chainTestCatalog("", map[string]types.ServeTokenPermissions{
+ bp := chainTestBlueprint("", map[string]types.ServeTokenPermissions{
"dev": {Permissions: types.ServePermissionSet{Global: []string{"create"}}}, // no get
})
- if err := playRead(cat, "servicerequest", "dev", types.ServeOpGet, "default", "x"); err == nil {
+ if err := playRead(bp, "servicerequest", "dev", types.ServeOpGet, "default", "x"); err == nil {
t.Fatal("expected the token to be denied get")
}
}
func TestPlayRead_AllowedGet(t *testing.T) {
- cat := chainTestCatalog("", nil) // no token restrictions -> allow all
- if err := playRead(cat, "servicerequest", "dev", types.ServeOpGet, "default", "x"); err != nil {
+ bp := chainTestBlueprint("", nil) // no token restrictions -> allow all
+ if err := playRead(bp, "servicerequest", "dev", types.ServeOpGet, "default", "x"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPlayRead_Delete(t *testing.T) {
- cat := chainTestCatalog("", nil)
- if err := playRead(cat, "servicerequest", "dev", types.ServeOpDelete, "default", "x"); err != nil {
+ bp := chainTestBlueprint("", nil)
+ if err := playRead(bp, "servicerequest", "dev", types.ServeOpDelete, "default", "x"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
diff --git a/cmd/cli/serve/print.go b/cmd/cli/serve/print.go
index f62504f7c..45734e538 100644
--- a/cmd/cli/serve/print.go
+++ b/cmd/cli/serve/print.go
@@ -44,10 +44,10 @@ func printPlayHeader(file, target, token, op string) {
fmt.Println()
}
-func printPlayHeaderForSimulate(catalog, intentFile, target, token string, cycles int) {
+func printPlayHeaderForSimulate(blueprint, intentFile, target, token string, cycles int) {
fmt.Println()
fmt.Printf("%s inrun simulate\n", cmdutil.Bold("▶"))
- fmt.Printf(" %s %s\n", cmdutil.Gray("catalog:"), cmdutil.Cyan(relPath(catalog)))
+ fmt.Printf(" %s %s\n", cmdutil.Gray("blueprint:"), cmdutil.Cyan(relPath(blueprint)))
fmt.Printf(" %s %s %s\n", cmdutil.Gray("intent: "), cmdutil.Cyan(relPath(intentFile)),
cmdutil.Gray(fmt.Sprintf("(target %s, token %s)", target, token)))
fmt.Printf(" %s %d\n", cmdutil.Gray("cycles: "), cycles)
diff --git a/cmd/cli/serve/serve.go b/cmd/cli/serve/serve.go
index 8fb8391a3..ceae16a24 100644
--- a/cmd/cli/serve/serve.go
+++ b/cmd/cli/serve/serve.go
@@ -11,7 +11,7 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog/validate"
+ "github.com/intentd/inrun/pkg/blueprint/validate"
"github.com/intentd/inrun/pkg/types"
"github.com/spf13/cobra"
)
@@ -32,14 +32,14 @@ what fields developers can submit, what the gateway builds, and what
callers see.
Subcommands:
- validate Validate Serve configuration in a Catalog
+ validate Validate Serve configuration in a Blueprint
schema Show the flat schema for a Serve target
fields List all Serve fields with their paths and types
tokens Show token permissions for a CRD
- targets List all Serve targets in a Catalog
- aliases List serve aliases in a Catalog
+ targets List all Serve targets in a Blueprint
+ aliases List serve aliases in a Blueprint
can-i Check if a token can perform an operation
- response Show the Serve response configuration`,
+ view Show the serve view a target returns`,
}
// ── serve validate ────────────────────────────────────────────────────────────
@@ -47,28 +47,28 @@ Subcommands:
var serveValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate Serve configuration",
- Long: `Validate Serve configuration in a Catalog.
+ Long: `Validate Serve configuration in a Blueprint.
This runs the same Serve-specific validations as inrun validate, but only for
-Serve concerns: fields, paths, tokens, response config, and namespace rules.
+Serve concerns: fields, paths, tokens, the view, and namespace rules.
-It does not check the full Catalog schema — only the Serve portions.
+It does not check the full Blueprint schema — only the Serve portions.
With --full, shows a detailed breakdown of the Serve configuration.`,
RunE: func(cmd *cobra.Command, args []string) error {
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
full, _ := cmd.Flags().GetBool("full")
- if err := validate.ValidateServe(cat); err != nil {
+ if err := validate.ValidateServe(bp); err != nil {
return err
}
if full {
- printServeValidationSummary(cat)
+ printServeValidationSummary(bp)
} else {
fmt.Printf("\n%s Serve configuration is valid\n", cmdutil.SuccessMark())
}
@@ -99,7 +99,7 @@ GET /api/v1/schema?target= endpoint.`,
return errRequiresCRDSelector
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -109,7 +109,7 @@ GET /api/v1/schema?target= endpoint.`,
if alias != "" && target == "" && kind == "" && name == "" {
var resolvedAlias string
- crd, resolvedAlias, err = resolveCRDByAnyTarget(cat, alias)
+ crd, resolvedAlias, err = resolveCRDByAnyTarget(bp, alias)
if err != nil {
return err
}
@@ -118,7 +118,7 @@ GET /api/v1/schema?target= endpoint.`,
}
header = fmt.Sprintf("\nSchema for: %s (alias: %s → target: %s)\n", crd.Name, alias, crd.ServeTarget())
} else {
- crd, err = resolveCRD(cat, target, kind, name)
+ crd, err = resolveCRD(bp, target, kind, name)
if err != nil {
return err
}
@@ -166,7 +166,7 @@ GET /api/v1/schema?target= endpoint.`,
var serveFieldsCmd = &cobra.Command{
Use: "fields",
Short: "List Serve fields with their paths and types",
- Long: `List Serve fields in a Catalog with their paths and types.
+ Long: `List Serve fields in a Blueprint with their paths and types.
This shows fields declared in serve.fields, serve.labels and serve.annotations
across all Serve-enabled CRDs.
@@ -187,7 +187,7 @@ With --target, --kind, or --name, shows fields for a specific CRD.`,
sortBy = "name"
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -199,7 +199,7 @@ With --target, --kind, or --name, shows fields for a specific CRD.`,
if alias != "" && target == "" && kind == "" && name == "" {
var resolvedAlias string
- crd, resolvedAlias, err = resolveCRDByAnyTarget(cat, alias)
+ crd, resolvedAlias, err = resolveCRDByAnyTarget(bp, alias)
if err != nil {
return err
}
@@ -208,7 +208,7 @@ With --target, --kind, or --name, shows fields for a specific CRD.`,
}
header = fmt.Sprintf("\nFields for: %s (alias: %s → target: %s)\n", crd.Name, alias, crd.ServeTarget())
} else {
- crd, err = resolveCRD(cat, target, kind, name)
+ crd, err = resolveCRD(bp, target, kind, name)
if err != nil {
return err
}
@@ -250,7 +250,7 @@ With --target, --kind, or --name, shows fields for a specific CRD.`,
}
// ── All fields across all CRDs ──────────────────────────────────────
- crds := cat.ServeEnabledCRDs()
+ crds := bp.ServeEnabledCRDs()
sort.Slice(crds, func(i, j int) bool {
if crds[i] == nil || crds[j] == nil {
return false
@@ -321,7 +321,7 @@ When it inherits from the CRD, the CRD-level tokens are shown with a note.`,
return errRequiresCRDSelector
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -330,7 +330,7 @@ When it inherits from the CRD, the CRD-level tokens are shown with a note.`,
if alias != "" && target == "" && kind == "" && name == "" {
// --alias alone: resolve CRD from alias name
var resolvedAlias string
- crd, resolvedAlias, err = resolveCRDByAnyTarget(cat, alias)
+ crd, resolvedAlias, err = resolveCRDByAnyTarget(bp, alias)
if err != nil {
return err
}
@@ -338,7 +338,7 @@ When it inherits from the CRD, the CRD-level tokens are shown with a note.`,
return fmt.Errorf("%s %q is a primary target, not an alias — use --target instead", cmdutil.FailureMark(), alias)
}
} else {
- crd, err = resolveCRD(cat, target, kind, name)
+ crd, err = resolveCRD(bp, target, kind, name)
if err != nil {
return err
}
@@ -407,12 +407,12 @@ When it inherits from the CRD, the CRD-level tokens are shown with a note.`,
var serveTargetsCmd = &cobra.Command{
Use: "targets",
- Short: "List all Serve targets in a Catalog",
- Long: `List all Serve-enabled targets in a Catalog.
+ Short: "List all Serve targets in a Blueprint",
+ Long: `List all Serve-enabled targets in a Blueprint.
This shows each target, its CRD kind, and whether it has fields defined.`,
RunE: func(cmd *cobra.Command, args []string) error {
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -425,7 +425,7 @@ This shows each target, its CRD kind, and whether it has fields defined.`,
Aliases int
}
- for _, crd := range cat.ServeEnabledCRDs() {
+ for _, crd := range bp.ServeEnabledCRDs() {
if crd == nil {
continue
}
@@ -526,7 +526,7 @@ Examples:
return errRequiresCRDSelector
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -539,7 +539,7 @@ Examples:
alias string
)
if target != "" {
- crd, alias, err = resolveCRDByAnyTarget(cat, target)
+ crd, alias, err = resolveCRDByAnyTarget(bp, target)
if err != nil {
return err
}
@@ -550,7 +550,7 @@ Examples:
}
} else if aliasFlag != "" && kind == "" && name == "" {
var resolvedAlias string
- crd, resolvedAlias, err = resolveCRDByAnyTarget(cat, aliasFlag)
+ crd, resolvedAlias, err = resolveCRDByAnyTarget(bp, aliasFlag)
if err != nil {
return err
}
@@ -559,7 +559,7 @@ Examples:
}
alias = resolvedAlias
} else {
- crd, err = resolveCRD(cat, target, kind, name)
+ crd, err = resolveCRD(bp, target, kind, name)
if err != nil {
return err
}
@@ -571,7 +571,7 @@ Examples:
}
// Verify the token exists at the gateway level first.
- gatewayTokens := cat.GatewayTokenNames()
+ gatewayTokens := bp.GatewayTokenNames()
tokenKnown := false
for _, t := range gatewayTokens {
if t == token {
@@ -614,26 +614,24 @@ Examples:
},
}
-// ── serve response ────────────────────────────────────────────────────────────
+// ── serve view ────────────────────────────────────────────────────────────
-var serveResponseCmd = &cobra.Command{
- Use: "response",
- Short: "Show the Serve response configuration",
- Long: `Show the Serve response configuration for a target.
+var serveViewCmd = &cobra.Command{
+ Use: "view",
+ Short: "Show the serve view a target returns",
+ Long: `Show the serve view for a target: what callers read back from the
+Gateway API, from serve.view. It shows:
-This displays what callers will see in the Gateway API response based on
-serve.config.response. It shows:
-
- - default: true/false
+ - fullObject: true/false
- Payload fields (with their template expressions)
- Excluded paths
- Poll URL configuration
-No cluster access is required — this reads the Catalog directly.
+No cluster access is required — this reads the Blueprint directly.
Examples:
- inrun serve response --target smartapp
- inrun serve response --target app --preview`,
+ inrun serve view --target smartapp
+ inrun serve view --target app --preview`,
RunE: func(cmd *cobra.Command, args []string) error {
preview, _ := cmd.Flags().GetBool("preview")
target, _ := cmd.Flags().GetString("target")
@@ -645,7 +643,7 @@ Examples:
return errRequiresCRDSelector
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -653,7 +651,7 @@ Examples:
var crd *types.CRDEntry
if alias != "" && target == "" && kind == "" && name == "" {
var resolvedAlias string
- crd, resolvedAlias, err = resolveCRDByAnyTarget(cat, alias)
+ crd, resolvedAlias, err = resolveCRDByAnyTarget(bp, alias)
if err != nil {
return err
}
@@ -661,7 +659,7 @@ Examples:
return fmt.Errorf("%s %q is a primary target, not an alias — use --target instead", cmdutil.FailureMark(), alias)
}
} else {
- crd, err = resolveCRD(cat, target, kind, name)
+ crd, err = resolveCRD(bp, target, kind, name)
if err != nil {
return err
}
@@ -671,27 +669,27 @@ Examples:
return fmt.Errorf("%s CRD %q is not Serve-enabled", cmdutil.FailureMark(), crd.Name)
}
- cfg := crd.ServeResponseConfigFor(alias)
+ cfg := crd.ServeViewFor(alias)
if cfg == nil {
if alias != "" {
- fmt.Printf("\nNo response configuration for alias %q (CRD: %s) — uses default CR response\n\n", alias, crd.Name)
+ fmt.Printf("\nNo view for alias %q (CRD: %s): the full CR is returned\n\n", alias, crd.Name)
} else {
- fmt.Printf("\nNo response configuration for CRD %q — callers receive the full CR as-is.\n", crd.Name)
- fmt.Println(" Add serve.config.response to customize what callers see.")
+ fmt.Printf("\nNo view for CRD %q: callers receive the full CR as-is.\n", crd.Name)
+ fmt.Println(" Add serve.view to customize what callers see.")
fmt.Println()
}
return nil
}
if alias != "" {
- fmt.Printf("\nResponse configuration for alias %q (CRD: %s)\n", alias, crd.Name)
+ fmt.Printf("\nView for alias %q (CRD: %s)\n", alias, crd.Name)
} else {
- fmt.Printf("\nResponse configuration for: %s (target: %s)\n", crd.Name, crd.ServeTarget())
+ fmt.Printf("\nView for: %s (target: %s)\n", crd.Name, crd.ServeTarget())
}
fmt.Printf("%s\n", strings.Repeat("─", 70))
- fmt.Printf("\ndefault: %v\n", cfg.UseDefault())
- if cfg.UseDefault() && !cfg.HasExclude() {
+ fmt.Printf("\nfullObject: %v\n", cfg.IncludesFullObject())
+ if cfg.IncludesFullObject() && !cfg.HasExclude() {
if cfg.HasPayload() {
fmt.Println(" Callers receive the full CR object plus the payload fields below.")
} else {
@@ -736,7 +734,7 @@ Examples:
if preview {
fmt.Printf("\n%s\n", strings.Repeat("─", 70))
- fmt.Println("\nResponse preview (templates shown as-is, not resolved):")
+ fmt.Println("\nView preview (templates shown as-is, not resolved):")
if cfg.HasPayload() {
fmt.Println()
@@ -782,11 +780,11 @@ Examples:
var serveAliasesCmd = &cobra.Command{
Use: "aliases",
- Short: "List serve aliases in a Catalog",
+ Short: "List serve aliases in a Blueprint",
Long: `List serve aliases across all serve-enabled CRDs, or for a specific CRD.
Aliases are additional named entry points for a CRD. Each alias can independently
-override token permissions and response configuration, falling back to CRD-level
+override token permissions and the view, falling back to CRD-level
defaults when not set.
With --target, --kind, or --name, shows aliases for that specific CRD only.
@@ -800,7 +798,7 @@ Examples:
name, _ := cmd.Flags().GetString("name")
alias, _ := cmd.Flags().GetString("alias")
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return err
}
@@ -811,12 +809,12 @@ Examples:
if alias != "" && target == "" && kind == "" && name == "" {
// Resolve by alias — show all aliases for the same CRD.
- crd, _, err = resolveCRDByAnyTarget(cat, alias)
+ crd, _, err = resolveCRDByAnyTarget(bp, alias)
if err != nil {
return err
}
} else {
- crd, err = resolveCRD(cat, target, kind, name)
+ crd, err = resolveCRD(bp, target, kind, name)
if err != nil {
return err
}
@@ -829,7 +827,7 @@ Examples:
}
// ── All CRDs ────────────────────────────────────────────────────────
- crds := cat.ServeEnabledCRDs()
+ crds := bp.ServeEnabledCRDs()
sort.Slice(crds, func(i, j int) bool { return crds[i].Name < crds[j].Name })
var total int
@@ -904,13 +902,13 @@ func init() {
serveAliasesCmd.Flags().StringP("alias", "a", "", "Alias name — shows all aliases for the same CRD")
serveCmd.AddCommand(serveAliasesCmd)
- // Response
- serveResponseCmd.Flags().StringP("target", "t", "", "Target to show response for")
- serveResponseCmd.Flags().StringP("kind", "k", "", "Kind to show response for")
- serveResponseCmd.Flags().StringP("name", "n", "", "CRD name to show response for")
- serveResponseCmd.Flags().StringP("alias", "a", "", "Alias to show effective response config for (can be used instead of --target)")
- serveResponseCmd.Flags().BoolP("preview", "p", false, "Show a sample response preview")
- serveCmd.AddCommand(serveResponseCmd)
+ // View
+ serveViewCmd.Flags().StringP("target", "t", "", "Target to show the view for")
+ serveViewCmd.Flags().StringP("kind", "k", "", "Kind to show the view for")
+ serveViewCmd.Flags().StringP("name", "n", "", "CRD name to show the view for")
+ serveViewCmd.Flags().StringP("alias", "a", "", "Alias to show the effective view for (can be used instead of --target)")
+ serveViewCmd.Flags().BoolP("preview", "p", false, "Show a sample view preview")
+ serveCmd.AddCommand(serveViewCmd)
cmdutil.RootCmd.AddCommand(serveCmd)
diff --git a/cmd/cli/serve/token.go b/cmd/cli/serve/token.go
index 95bd2c95d..2c4cb3b1d 100644
--- a/cmd/cli/serve/token.go
+++ b/cmd/cli/serve/token.go
@@ -40,9 +40,9 @@ Subcommands:
var tokenVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify a JWT against the configured token entries",
- Long: `Verify a JWT against gateway.api.auth.tokens in the catalog.
+ Long: `Verify a JWT against gateway.api.auth.tokens in the blueprint.
-Local mode (default): loads the catalog, fetches JWKS from the real provider,
+Local mode (default): loads the blueprint, fetches JWKS from the real provider,
verifies the token signature and claims, and shows which entry matched.
Live mode (--api): sends the token to a running gateway and reports accept/reject.
@@ -50,7 +50,7 @@ Use inrun proxy to expose the gateway locally first.
Examples:
inrun token verify
- inrun token verify -f catalog.yaml -t token.jwt
+ inrun token verify -f blueprint.yaml -t token.jwt
inrun token verify --api https://gateway.myorg.io -t token.jwt
inrun token verify --api http://localhost:8443`,
SilenceUsage: true,
@@ -112,17 +112,17 @@ func runTokenVerifyLocal(cmd *cobra.Command, jwt, audienceOverride, tokenFile st
return fmt.Errorf("%s %q does not look like a JWT — could not extract iss claim", cmdutil.FailureMark(), tokenFile)
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return fmt.Errorf("%s %w", cmdutil.FailureMark(), err)
}
- if !cat.IsGatewayEnabled() || !cat.Gateway.HasAPI() || cat.Gateway.API.Auth.Empty() {
- return fmt.Errorf("%s no gateway token auth configured in this catalog", cmdutil.FailureMark())
+ if !bp.IsGatewayEnabled() || !bp.Gateway.HasAPI() || bp.Gateway.API.Auth.Empty() {
+ return fmt.Errorf("%s no gateway token auth configured in this blueprint", cmdutil.FailureMark())
}
var candidates []types.APIToken
- for _, t := range cat.Gateway.API.Auth.Tokens {
+ for _, t := range bp.Gateway.API.Auth.Tokens {
if t.IsOIDC() && t.OIDCIssuer() == iss {
candidates = append(candidates, t)
}
@@ -206,7 +206,7 @@ Vault, which uses a non-standard discovery path.
Example:
inrun token probe --name vault-ci
- inrun token probe -f catalog.yaml --name gh-ci`,
+ inrun token probe -f blueprint.yaml --name gh-ci`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
@@ -214,19 +214,19 @@ Example:
return fmt.Errorf("%s --name is required", cmdutil.FailureMark())
}
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return fmt.Errorf("%s %w", cmdutil.FailureMark(), err)
}
- if !cat.IsGatewayEnabled() || !cat.Gateway.HasAPI() || cat.Gateway.API.Auth.Empty() {
- return fmt.Errorf("%s no gateway token auth configured in this catalog", cmdutil.FailureMark())
+ if !bp.IsGatewayEnabled() || !bp.Gateway.HasAPI() || bp.Gateway.API.Auth.Empty() {
+ return fmt.Errorf("%s no gateway token auth configured in this blueprint", cmdutil.FailureMark())
}
var entry *types.APIToken
- for i, t := range cat.Gateway.API.Auth.Tokens {
+ for i, t := range bp.Gateway.API.Auth.Tokens {
if t.Name == name {
- entry = &cat.Gateway.API.Auth.Tokens[i]
+ entry = &bp.Gateway.API.Auth.Tokens[i]
break
}
}
@@ -328,20 +328,20 @@ Shows each token's name, type, provider kind, and allow summary.
Example:
inrun token list
- inrun token list -f catalog.yaml`,
+ inrun token list -f blueprint.yaml`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- cat, err := cmdutil.BuildCatalog(cmd)
+ bp, err := cmdutil.BuildBlueprint(cmd)
if err != nil {
return fmt.Errorf("%s %w", cmdutil.FailureMark(), err)
}
- if !cat.IsGatewayEnabled() || !cat.Gateway.HasAPI() || cat.Gateway.API.Auth.Empty() {
+ if !bp.IsGatewayEnabled() || !bp.Gateway.HasAPI() || bp.Gateway.API.Auth.Empty() {
fmt.Printf("\n %s no gateway token auth configured\n\n", cmdutil.Dim("—"))
return nil
}
- tokens := cat.Gateway.API.Auth.Tokens
+ tokens := bp.Gateway.API.Auth.Tokens
fmt.Printf("\n %s %s\n\n", cmdutil.Gray("token list"), cmdutil.Dim(fmt.Sprintf("%d entries", len(tokens))))
type row struct{ name, typ, provider, allow string }
diff --git a/cmd/cli/testsuite/e2e.go b/cmd/cli/testsuite/e2e.go
index 4a4b34e2d..302028be6 100644
--- a/cmd/cli/testsuite/e2e.go
+++ b/cmd/cli/testsuite/e2e.go
@@ -13,7 +13,7 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
"github.com/intentd/inrun/cmd/cli/validate"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/registry/e2e"
"gopkg.in/yaml.v3"
@@ -49,7 +49,7 @@ Discovery mode — runs all *e2e.yaml files found recursively (skips pure aggreg
file = args[0]
}
if file == "" {
- file = cmdutil.FindFile(cmdutil.FileE2e, cmdutil.DirTest)
+ file = cmdutil.FindFile(cmdutil.FileE2e)
}
if file == "" {
return fmt.Errorf("no %s found here or in %s/ — pass -f ", cmdutil.FileE2e, cmdutil.DirTest)
@@ -65,10 +65,7 @@ Discovery mode — runs all *e2e.yaml files found recursively (skips pure aggreg
}
valuesFiles, _ := cmd.Flags().GetStringSlice("values")
helmArgRaw, _ := cmd.Flags().GetStringSlice("set")
- var helmArgs []string
- for _, arg := range helmArgRaw {
- helmArgs = append(helmArgs, "--set", arg)
- }
+ helmArgs := cmdutil.HelmSetArgs(helmArgRaw)
devServer, _ := cmd.Flags().GetBool("dev-server")
noRuntime, _ := cmd.Flags().GetBool("no-runtime")
wait, _ := cmd.Flags().GetString("wait")
@@ -207,13 +204,13 @@ type crdInfo struct {
var e2eInitCmd = &cobra.Command{
Use: "init",
- Short: "Scaffold an e2e.yaml from the current Catalog",
- Long: `Reads the Catalog in the current directory and generates a best-practice
+ Short: "Scaffold an e2e.yaml from the current Blueprint",
+ Long: `Reads the Blueprint in the current directory and generates a best-practice
e2e.yaml skeleton: CR created → resources created → cleanup verified.
-No cluster is needed — it reads only the Catalog for CRD names and kinds.
+No cluster is needed — it reads only the Blueprint for CRD names and kinds.
- inrun e2e init # auto-detect catalog.yaml
- inrun e2e init -f catalog.yaml # explicit
+ inrun e2e init # auto-detect blueprint.yaml
+ inrun e2e init -f blueprint.yaml # explicit
inrun e2e init --force # overwrite existing e2e.yaml
inrun e2e init --dry-run # preview without writing
inrun e2e init --suite # aggregate all e2e.yaml files under .
@@ -226,21 +223,21 @@ No cluster is needed — it reads only the Catalog for CRD names and kinds.
},
}
-// e2eInitScaffold reads the Catalog and writes an e2e.yaml scaffold.
+// e2eInitScaffold reads the Blueprint and writes an e2e.yaml scaffold.
func e2eInitScaffold(cmd *cobra.Command) error {
- catalogFile, _ := cmd.Flags().GetString("file")
+ blueprintFile, _ := cmd.Flags().GetString("file")
force, _ := cmd.Flags().GetBool("force")
dryRun, _ := cmd.Flags().GetBool("dry-run")
var err error
- catalogFile, err = cmdutil.ResolveCatalogFile(catalogFile)
+ blueprintFile, err = cmdutil.ResolveBlueprintFile(blueprintFile)
if err != nil {
return err
}
- cat, err := catalog.ParseFile(catalogFile)
+ bp, err := blueprint.ParseFile(blueprintFile)
if err != nil {
- return fmt.Errorf("parsing Catalog: %w", err)
+ return fmt.Errorf("parsing Blueprint: %w", err)
}
outPath := filepath.Join(cmdutil.DirTest, cmdutil.FileE2e)
@@ -251,14 +248,14 @@ func e2eInitScaffold(cmd *cobra.Command) error {
}
// Paths in the spec are relative to the spec file, in test/.
- relCatalog := relFromDir(cmdutil.DirTest, catalogFile)
- relCRD := relFromDir(cmdutil.DirTest, projectFile(catalogFile, cmdutil.FileCrd, cmdutil.DirManifests))
- relCR := relFromDir(cmdutil.DirTest, projectFile(catalogFile, cmdutil.FileCr, cmdutil.DirManifests))
+ relBlueprint := relFromDir(cmdutil.DirTest, blueprintFile)
+ relCRD := relFromDir(cmdutil.DirTest, projectFile(blueprintFile, cmdutil.FileCrd))
+ relCR := relFromDir(cmdutil.DirTest, projectFile(blueprintFile, cmdutil.FileCr))
- crdNames := cat.CRDNames()
+ crdNames := bp.CRDNames()
var crds []crdInfo
for _, n := range crdNames {
- entry, ok := cat.CRDEntry(n)
+ entry, ok := bp.CRDEntry(n)
if !ok {
continue
}
@@ -266,7 +263,7 @@ func e2eInitScaffold(cmd *cobra.Command) error {
}
sort.Slice(crds, func(i, j int) bool { return crds[i].name < crds[j].name })
- output := buildE2EScaffoldYAML(cat.Metadata().Name, relCatalog, relCRD, relCR, crds)
+ output := buildE2EScaffoldYAML(bp.Metadata().Name, relBlueprint, relCRD, relCR, crds)
if dryRun {
fmt.Print(output)
@@ -393,7 +390,7 @@ func e2eInitSuite(cmd *cobra.Command, args []string) error {
// buildE2EScaffoldYAML returns the scaffold YAML string for inrun e2e init.
// Comments are injected directly — yaml.Marshal cannot preserve them.
-func buildE2EScaffoldYAML(catalogName, catalogFile, crdFile, crFile string, crds []crdInfo) string {
+func buildE2EScaffoldYAML(blueprintName, blueprintFile, crdFile, crFile string, crds []crdInfo) string {
var b strings.Builder
w := func(s string) { b.WriteString(s + "\n") }
@@ -406,27 +403,27 @@ func buildE2EScaffoldYAML(catalogName, catalogFile, crdFile, crFile string, crds
w("apiVersion: inrun.dev/v1")
w("kind: E2E")
w("metadata:")
- w(" name: " + catalogName + "-e2e")
+ w(" name: " + blueprintName + "-e2e")
w(` description: "Generated by inrun e2e init — edit to refine"`)
w("")
w("spec:")
- w(" catalog: " + catalogFile)
+ w(" blueprint: " + blueprintFile)
w(" crd: " + crdFile)
w(" cr: " + crFile)
w("")
w(" cluster:")
w(" provider: kind")
- w(" name: " + catalogName + "-e2e")
+ w(" name: " + blueprintName + "-e2e")
w(" reuse: false")
if len(crds) > 1 {
w("")
- w(" # Multiple CRDs in this Catalog. Apply the remaining CRDs and CRs via setup.")
+ w(" # Multiple CRDs in this Blueprint. Apply the remaining CRDs and CRs via setup.")
w(" setup:")
w(" apply:")
w(" - ./other-crds.yaml # CRD definitions for the remaining kinds")
w(" - ./other-crs.yaml # CR instances for the remaining kinds")
- w(" # Additional CRDs in this Catalog:")
+ w(" # Additional CRDs in this Blueprint:")
for _, c := range crds[1:] {
w(" # - " + c.kind)
}
@@ -484,7 +481,7 @@ func init() {
cmdutil.RootCmd.AddCommand(e2eCmd)
e2eCmd.AddCommand(e2eInitCmd)
- e2eInitCmd.Flags().StringP("file", "f", "", "Path to catalog.yaml or stack.yaml")
+ e2eInitCmd.Flags().StringP("file", "f", "", "Path to blueprint.yaml or stack.yaml")
e2eInitCmd.Flags().Bool("force", false, "Overwrite existing e2e.yaml")
e2eInitCmd.Flags().Bool("dry-run", false, "Print the generated e2e.yaml to stdout instead of writing the file")
e2eInitCmd.Flags().Bool("suite", false, "Aggregate all e2e.yaml leaf files found under the given dir (default: .)")
@@ -511,19 +508,8 @@ func init() {
cmdutil.ShadowGlobalCommandFlags(e2eCmd)
}
-// chartFlag returns --chart as an absolute path, or "" when unset. The path
-// must be a chart directory: e2e may change directory before installing.
+// chartFlag returns --chart as an absolute chart directory, or "" when unset.
func chartFlag(cmd *cobra.Command) (string, error) {
chart, _ := cmd.Flags().GetString("chart")
- if chart == "" {
- return "", nil
- }
- abs, err := filepath.Abs(chart)
- if err != nil {
- return "", fmt.Errorf("--chart %q: %w", chart, err)
- }
- if _, err := os.Stat(filepath.Join(abs, "Chart.yaml")); err != nil {
- return "", fmt.Errorf("--chart %q: not a Helm chart directory (no Chart.yaml)", chart)
- }
- return abs, nil
+ return cmdutil.ChartDir(chart)
}
diff --git a/cmd/cli/testsuite/helper.go b/cmd/cli/testsuite/helper.go
index 7913f49a0..c7f4eb6c8 100644
--- a/cmd/cli/testsuite/helper.go
+++ b/cmd/cli/testsuite/helper.go
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/registry/simulate"
"github.com/intentd/inrun/pkg/types"
@@ -51,19 +51,19 @@ func readSpecCRs(dir string, paths []string) (map[string][]*unstructured.Unstruc
var PlayIntent func(PlayRequest) (*unstructured.Unstructured, error)
type PlayRequest struct {
- Catalog *catalog.Catalog // the catalog the intent is resolved against
- CatalogFile string // path to the catalog.yaml
- IntentFile string // path to the intent YAML/JSON
- Token string // serve token to check the op against
- Op types.ServeOperation // create / update / delete
- Cycles int // reconcile cycles for simulate
- Verbose bool // print each stage
+ Blueprint *blueprint.Blueprint // the blueprint the intent is resolved against
+ BlueprintFile string // path to the blueprint.yaml
+ IntentFile string // path to the intent YAML/JSON
+ Token string // serve token to check the op against
+ Op types.ServeOperation // create / update / delete
+ Cycles int // reconcile cycles for simulate
+ Verbose bool // print each stage
}
-// CatalogCRDPaths returns the CRD files the Catalog's CRDs point at with
-// crdFile, resolved like the Catalog parser does. --envtest installs them when
+// BlueprintCRDPaths returns the CRD files the Blueprint's CRDs point at with
+// crdFile, resolved like the Blueprint parser does. --envtest installs them when
// a Simulate spec lists no crd or crdFiles.
-func CatalogCRDPaths(m *merger.Merger) []string {
+func BlueprintCRDPaths(m *merger.Merger) []string {
dir := m.FirstEntryDir()
seen := map[string]bool{}
var paths []string
diff --git a/cmd/cli/testsuite/simulate.go b/cmd/cli/testsuite/simulate.go
index ffcf9f013..1985e653a 100644
--- a/cmd/cli/testsuite/simulate.go
+++ b/cmd/cli/testsuite/simulate.go
@@ -7,18 +7,20 @@ import (
"context"
"errors"
"fmt"
- "github.com/intentd/inrun/pkg/logger"
- "github.com/intentd/inrun/pkg/utils"
"os"
"path/filepath"
"strings"
"time"
+ "github.com/intentd/inrun/pkg/logger"
+ "github.com/intentd/inrun/pkg/registry"
+ "github.com/intentd/inrun/pkg/util/term"
+
"github.com/intentd/inrun/cmd/cli/cmdutil"
"github.com/intentd/inrun/cmd/cli/validate"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
"github.com/intentd/inrun/pkg/merger"
"github.com/intentd/inrun/pkg/registry/e2e"
"github.com/intentd/inrun/pkg/registry/simulate"
@@ -52,7 +54,7 @@ should produce so the run is repeatable and verifiable:
inrun simulate # simulate.yaml auto-detected
inrun simulate -f simulate.yaml # explicit — assert mode when expect: is set
- inrun simulate -f catalog.yaml --cr cr.yaml # direct flags; op-print only
+ inrun simulate -f blueprint.yaml --cr cr.yaml # direct flags; op-print only
inrun simulate ./... # discovers all simulate.yaml files recursively`,
RunE: func(cmd *cobra.Command, args []string) error {
cliOpts := CliSimulateOptions{}
@@ -78,62 +80,62 @@ should produce so the run is repeatable and verifiable:
return runSimulateDiscovery(cmd.Context(), ".", skipRaw, cliOpts)
}
- catalogFile, _ := cmd.Flags().GetString("file")
- if catalogFile == "" {
- // Auto-detect: simulate.yaml (here, then test/) → catalog.yaml/stack.yaml
- switch sim := cmdutil.FindFile(cmdutil.FileSimulate, cmdutil.DirTest); {
+ blueprintFile, _ := cmd.Flags().GetString("file")
+ if blueprintFile == "" {
+ // Auto-detect: simulate.yaml (here, then test/) → blueprint.yaml/stack.yaml
+ switch sim := cmdutil.FindFile(cmdutil.FileSimulate); {
case sim != "":
- catalogFile = sim
+ blueprintFile = sim
default:
if d := cmdutil.DefaultFilePaths(); len(d) > 0 {
- catalogFile = d[0]
+ blueprintFile = d[0]
}
}
}
- if catalogFile == "" {
- return fmt.Errorf(cmdutil.ErrNoCatalog)
+ if blueprintFile == "" {
+ return fmt.Errorf(cmdutil.ErrNoBlueprint)
}
// Simulate kind: assert mode
- if simulate.IsSimulateDoc(catalogFile) {
- return RunSimulateFromSpec(cmd.Context(), catalogFile, cliOpts)
+ if simulate.IsSimulateDoc(blueprintFile) {
+ return RunSimulateFromSpec(cmd.Context(), blueprintFile, cliOpts)
}
// Reject E2E files with a clear message
- if e2e.IsE2EDoc(catalogFile) {
- return fmt.Errorf("%s is an E2E file — use 'inrun e2e' for cluster testing, or run 'inrun simulate init' to generate a simulate.yaml", catalogFile)
+ if e2e.IsE2EDoc(blueprintFile) {
+ return fmt.Errorf("%s is an E2E file — use 'inrun e2e' for cluster testing, or run 'inrun simulate init' to generate a simulate.yaml", blueprintFile)
}
crFile, _ := cmd.Flags().GetString("cr")
if crFile == "" {
- crFile = cmdutil.FindFile(cmdutil.FileCr, cmdutil.DirManifests)
+ crFile = cmdutil.FindFile(cmdutil.FileCr)
}
if crFile == "" {
return fmt.Errorf("--cr is required")
}
- return RunSimulate(cmd.Context(), catalogFile, crFile, cliOpts)
+ return RunSimulate(cmd.Context(), blueprintFile, crFile, cliOpts)
},
}
-func RunSimulate(ctx context.Context, catalogFile, crFile string, cliOpts CliSimulateOptions) error {
+func RunSimulate(ctx context.Context, blueprintFile, crFile string, cliOpts CliSimulateOptions) error {
maxCycles := cliOpts.MaxCycles
if maxCycles <= 0 {
maxCycles = 10
}
- m := merger.New(catalogFile)
+ m := merger.New(blueprintFile)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging Catalog: %w", err)
+ return fmt.Errorf("merging Blueprint: %w", err)
}
- crdPaths := CatalogCRDPaths(m)
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
+ crdPaths := BlueprintCRDPaths(m)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
if err != nil {
- var typedErr *catalog.TypedOperatorError
+ var typedErr *blueprint.TypedOperatorError
if errors.As(err, &typedErr) {
cmdutil.PrintTypedOperatorHint(typedErr, "inrun simulate")
}
- return fmt.Errorf("parsing Catalog: %w", err)
+ return fmt.Errorf("parsing Blueprint: %w", err)
}
crData, err := cmdutil.ReadLocal(crFile)
@@ -152,13 +154,13 @@ func RunSimulate(ctx context.Context, catalogFile, crFile string, cliOpts CliSim
if cliOpts.CRDName != "" {
targets = []string{cliOpts.CRDName}
} else {
- targets = cat.CRDNames()
+ targets = bp.CRDNames()
}
baseOpts := simulate.RunOptions{SkipExternal: cliOpts.SkipExternal, Target: cliOpts.Target}
for _, name := range targets {
- crdEntry, ok := cat.CRDEntry(name)
+ crdEntry, ok := bp.CRDEntry(name)
if !ok {
continue
}
@@ -173,14 +175,14 @@ func RunSimulate(ctx context.Context, catalogFile, crFile string, cliOpts CliSim
crdOpts := baseOpts
crdOpts.Peers = in.Peers
crdOpts.ExistingInstances = in.Existing
- if err := SimulateOne(ctx, cat, name, in.CR, maxCycles, crdOpts, cliOpts, crdPaths, nil); err != nil {
+ if err := SimulateOne(ctx, bp, name, in.CR, maxCycles, crdOpts, cliOpts, crdPaths, nil); err != nil {
return err
}
}
return nil
}
-func SimulateOne(ctx context.Context, cat *catalog.Catalog, crdName string, cr *unstructured.Unstructured, maxCycles int, opts simulate.RunOptions, cliOpts CliSimulateOptions, crdPaths []string, expect *types.SimulateExpect) error {
+func SimulateOne(ctx context.Context, bp *blueprint.Blueprint, crdName string, cr *unstructured.Unstructured, maxCycles int, opts simulate.RunOptions, cliOpts CliSimulateOptions, crdPaths []string, expect *types.SimulateExpect) error {
if cliOpts.UseEnvtest {
version := cliOpts.K8sVersion
if version == "" {
@@ -192,7 +194,7 @@ func SimulateOne(ctx context.Context, cat *catalog.Catalog, crdName string, cr *
}
// Emit notes for operatorBox blocks that cannot execute in the fake cluster.
- crdEntry, _ := cat.CRDEntry(crdName)
+ crdEntry, _ := bp.CRDEntry(crdName)
if crdEntry.Box().EffectiveOnReconcile() != nil && len(crdEntry.Box().EffectiveOnReconcile().External) > 0 {
if opts.SkipExternal {
fmt.Printf(" %s\n", cmdutil.Dim("note: external: calls stubbed — result fields will be empty"))
@@ -208,7 +210,7 @@ func SimulateOne(ctx context.Context, cat *catalog.Catalog, crdName string, cr *
// The reconciler's own logs show only with --verbose; errors always do.
// With --verbose there is no spinner, so log lines don't break into it.
- var spin *utils.Spinner
+ var spin *term.Spinner
if cliOpts.Verbose {
fmt.Printf("Running %d cycles...\n", maxCycles)
} else {
@@ -222,9 +224,9 @@ func SimulateOne(ctx context.Context, cat *catalog.Catalog, crdName string, cr *
if len(crdPaths) == 0 {
return fmt.Errorf("%s --envtest requires spec.crd or spec.crdFiles to be set", cmdutil.FailureMark())
}
- result, err = simulate.RunWithEnvtest(ctx, cat, crdName, cr, maxCycles, opts, crdPaths, cliOpts.K8sVersion)
+ result, err = simulate.RunWithEnvtest(ctx, bp, crdName, cr, maxCycles, opts, crdPaths, cliOpts.K8sVersion)
} else {
- result, err = simulate.Run(ctx, cat, crdName, cr, maxCycles, opts)
+ result, err = simulate.Run(ctx, bp, crdName, cr, maxCycles, opts)
}
if err != nil {
if spin != nil {
@@ -368,7 +370,7 @@ func RunSimulateFromSpec(ctx context.Context, path string, cliOpts CliSimulateOp
Target: effectiveTarget,
}
- catalogPath := filepath.Join(dir, doc.Spec.Catalog)
+ blueprintPath := filepath.Join(dir, doc.Spec.Blueprint)
// Resolve CRD paths (for --envtest) relative to the simulate.yaml directory.
crdPaths := make([]string, 0, len(doc.Spec.AllCRDPaths()))
@@ -380,21 +382,21 @@ func RunSimulateFromSpec(ctx context.Context, path string, cliOpts CliSimulateOp
crdPaths = append(crdPaths, abs)
}
- m := merger.New(catalogPath)
+ m := merger.New(blueprintPath)
if err := m.Merge(); err != nil {
- return fmt.Errorf("merging Catalog: %w", err)
+ return fmt.Errorf("merging Blueprint: %w", err)
}
if len(crdPaths) == 0 {
- crdPaths = CatalogCRDPaths(m)
+ crdPaths = BlueprintCRDPaths(m)
}
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, m)
if err != nil {
- var typedErr *catalog.TypedOperatorError
+ var typedErr *blueprint.TypedOperatorError
if errors.As(err, &typedErr) {
cmdutil.PrintTypedOperatorHint(typedErr, "inrun simulate")
}
- return fmt.Errorf("parsing Catalog: %w", err)
+ return fmt.Errorf("parsing Blueprint: %w", err)
}
var crs map[string][]*unstructured.Unstructured
@@ -404,13 +406,13 @@ func RunSimulateFromSpec(ctx context.Context, path string, cliOpts CliSimulateOp
return fmt.Errorf("%s: spec.intent is not supported in this build", path)
}
obj, err := PlayIntent(PlayRequest{
- Catalog: cat,
- CatalogFile: catalogPath,
- IntentFile: filepath.Join(dir, doc.Spec.Intent),
- Token: doc.Spec.Token,
- Op: types.ServeOpCreate,
- Cycles: doc.Spec.Cycles,
- Verbose: cliOpts.Verbose,
+ Blueprint: bp,
+ BlueprintFile: blueprintPath,
+ IntentFile: filepath.Join(dir, doc.Spec.Intent),
+ Token: doc.Spec.Token,
+ Op: types.ServeOpCreate,
+ Cycles: doc.Spec.Cycles,
+ Verbose: cliOpts.Verbose,
})
if err != nil {
return fmt.Errorf("playing %s: %w", doc.Spec.Intent, err)
@@ -428,12 +430,12 @@ func RunSimulateFromSpec(ctx context.Context, path string, cliOpts CliSimulateOp
if cliOpts.CRDName != "" {
targets = []string{cliOpts.CRDName}
} else {
- targets = cat.CRDNames()
+ targets = bp.CRDNames()
}
var failed []string
for _, name := range targets {
- crdEntry, ok := cat.CRDEntry(name)
+ crdEntry, ok := bp.CRDEntry(name)
if !ok {
continue
}
@@ -448,7 +450,7 @@ func RunSimulateFromSpec(ctx context.Context, path string, cliOpts CliSimulateOp
crdOpts.Peers = in.Peers
crdOpts.ExistingInstances = in.Existing
expect := simulate.ExpectForCRD(doc.Spec.Expect, name)
- if err := SimulateOne(ctx, cat, name, in.CR, cycles, crdOpts, cliOpts, crdPaths, expect); err != nil {
+ if err := SimulateOne(ctx, bp, name, in.CR, cycles, crdOpts, cliOpts, crdPaths, expect); err != nil {
failed = append(failed, name)
}
}
@@ -565,7 +567,7 @@ var simulateInitCmd = &cobra.Command{
observed cycle-1 create operations as expect: rules. Edit and refine from there.
inrun simulate init
- inrun simulate init -f catalog.yaml --cr cr.yaml
+ inrun simulate init -f blueprint.yaml --cr cr.yaml
inrun simulate init --force # overwrite existing simulate.yaml
inrun simulate init --suite # aggregate all simulate.yaml files under .
inrun simulate init --suite ./examples/ # aggregate under a specific dir`,
@@ -574,25 +576,25 @@ observed cycle-1 create operations as expect: rules. Edit and refine from there.
return simulateInitSuite(cmd, args)
}
- catalogFile, _ := cmd.Flags().GetString("file")
+ blueprintFile, _ := cmd.Flags().GetString("file")
crFile, _ := cmd.Flags().GetString("cr")
force, _ := cmd.Flags().GetBool("force")
dryRun, _ := cmd.Flags().GetBool("dry-run")
var err error
- catalogFile, err = cmdutil.ResolveCatalogFile(catalogFile)
+ blueprintFile, err = cmdutil.ResolveBlueprintFile(blueprintFile)
if err != nil {
return err
}
if crFile == "" {
- crFile = projectFile(catalogFile, cmdutil.FileCr, cmdutil.DirManifests)
+ crFile = projectFile(blueprintFile, cmdutil.FileCr)
} else if abs, err := filepath.Abs(crFile); err == nil {
crFile = abs
}
- cat, err := catalog.ParseFile(catalogFile)
+ bp, err := blueprint.ParseFile(blueprintFile)
if err != nil {
- return fmt.Errorf("parsing Catalog: %w", err)
+ return fmt.Errorf("parsing Blueprint: %w", err)
}
crData, err := cmdutil.ReadLocal(crFile)
if err != nil {
@@ -611,12 +613,12 @@ observed cycle-1 create operations as expect: rules. Edit and refine from there.
}
opts := simulate.RunOptions{}
- targets := cat.CRDNames()
+ targets := bp.CRDNames()
var results []crdOps
for _, name := range targets {
- crdEntry, ok := cat.CRDEntry(name)
+ crdEntry, ok := bp.CRDEntry(name)
if !ok {
continue
}
@@ -627,7 +629,7 @@ observed cycle-1 create operations as expect: rules. Edit and refine from there.
crdOpts := opts
crdOpts.Peers = in.Peers
crdOpts.ExistingInstances = in.Existing
- result, err := simulate.Run(cmd.Context(), cat, name, in.CR, 10, crdOpts)
+ result, err := simulate.Run(cmd.Context(), bp, name, in.CR, 10, crdOpts)
if err != nil && crdEntry.WithRemoteDecl() {
fmt.Printf(" %s %s: remote reconciler unreachable — start it and re-run (%v)\n", cmdutil.Dim("note:"), name, err)
continue
@@ -645,10 +647,10 @@ observed cycle-1 create operations as expect: rules. Edit and refine from there.
}
// Paths in the spec are relative to the spec file, so the file is portable.
- relCatalog := relFromDir(cmdutil.DirTest, catalogFile)
+ relBlueprint := relFromDir(cmdutil.DirTest, blueprintFile)
relCR := relFromDir(cmdutil.DirTest, crFile)
- doc := generateSimulateDoc(relCatalog, relCR, cat.Metadata().Name, results)
+ doc := generateSimulateDoc(relBlueprint, relCR, bp.Metadata().Name, results)
var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
@@ -781,7 +783,7 @@ func simulateInitSuite(cmd *cobra.Command, args []string) error {
}
// generateSimulateDoc builds a Simulate document from observed cycle-1 creates.
-func generateSimulateDoc(catalogPath, crPath, catalogName string, results []crdOps) map[string]interface{} {
+func generateSimulateDoc(blueprintPath, crPath, blueprintName string, results []crdOps) map[string]interface{} {
trueVal := true
makeOps := func(ops []simulate.Op) []map[string]interface{} {
@@ -801,9 +803,9 @@ func generateSimulateDoc(catalogPath, crPath, catalogName string, results []crdO
}
spec := map[string]interface{}{
- "catalog": catalogPath,
- "cr": crPath,
- "cycles": 5,
+ "blueprint": blueprintPath,
+ "cr": crPath,
+ "cycles": 5,
}
if len(results) == 1 {
@@ -837,7 +839,7 @@ func generateSimulateDoc(catalogPath, crPath, catalogName string, results []crdO
"apiVersion": "inrun.dev/v1",
"kind": "Simulate",
"metadata": map[string]interface{}{
- "name": catalogName + "-sim",
+ "name": blueprintName + "-sim",
"description": "Generated by inrun simulate init — edit to refine",
},
"spec": spec,
@@ -902,16 +904,16 @@ func init() {
cmdutil.RootCmd.AddCommand(simulateCmd)
simulateCmd.AddCommand(simulateInitCmd)
- simulateInitCmd.Flags().StringP("file", "f", "", "Path to catalog.yaml or stack.yaml")
+ simulateInitCmd.Flags().StringP("file", "f", "", "Path to blueprint.yaml or stack.yaml")
simulateInitCmd.Flags().String("cr", "", "Path to the CR YAML file")
simulateInitCmd.Flags().Bool("force", false, "Overwrite existing simulate.yaml")
simulateInitCmd.Flags().Bool("dry-run", false, "Print the generated simulate.yaml to stdout instead of writing the file")
simulateInitCmd.Flags().Bool("suite", false, "Aggregate all simulate.yaml leaf files found under the given dir (default: .)")
simulateInitCmd.Flags().StringSlice("skip", []string{}, "Comma-separated path patterns to exclude from suite discovery")
- simulateCmd.Flags().StringP("file", "f", "", "Path to catalog.yaml")
+ simulateCmd.Flags().StringP("file", "f", "", "Path to blueprint.yaml")
simulateCmd.Flags().String("cr", "", "Path to the CR YAML file to simulate")
- simulateCmd.Flags().String("crd", "", "CRD name to simulate (default: all CRDs in Catalog)")
+ simulateCmd.Flags().String("crd", "", "CRD name to simulate (default: all CRDs in Blueprint)")
simulateCmd.Flags().Int("cycles", 10, "Maximum number of reconcile cycles")
simulateCmd.Flags().StringP("target", "t", "", "Target that provides the operatorbox for simulate")
simulateCmd.Flags().StringSlice("skip", []string{}, "Comma-separated path patterns to skip during ./... discovery (e.g. vendor,cr-e2e.yaml)")
@@ -927,12 +929,12 @@ func init() {
cmdutil.ShadowGlobalCommandFlags(simulateCmd)
}
-// projectFile returns name next to the Catalog, or in its sub directory dir
-// when only that exists. The path next to the Catalog is the fallback.
-func projectFile(catalogFile, name, dir string) string {
- base := filepath.Dir(catalogFile)
- if p := filepath.Join(base, dir, name); cmdutil.FileExists(p) && !cmdutil.FileExists(filepath.Join(base, name)) {
- return p
+// projectFile returns where name is in the Blueprint's project: next to it,
+// else its place in the standard layout. Next to it is the fallback.
+func projectFile(blueprintFile, name string) string {
+ base := filepath.Dir(blueprintFile)
+ if rel := registry.FindPatternFile(base, name); rel != "" {
+ return filepath.Join(base, rel)
}
return filepath.Join(base, name)
}
diff --git a/cmd/cli/validate/doc.go b/cmd/cli/validate/doc.go
index 89cb812e9..7df3cd725 100644
--- a/cmd/cli/validate/doc.go
+++ b/cmd/cli/validate/doc.go
@@ -1,4 +1,4 @@
-// Package validate implements the validate command for Catalogs, Stacks,
+// Package validate implements the validate command for Blueprints, Stacks,
// modules and test specs, and the template command that renders the fully
-// expanded runtime Catalog.
+// expanded runtime Blueprint.
package validate
diff --git a/cmd/cli/validate/output.go b/cmd/cli/validate/output.go
index d4931010b..e6a930793 100644
--- a/cmd/cli/validate/output.go
+++ b/cmd/cli/validate/output.go
@@ -4,7 +4,7 @@ package validate
// printEnrichmentReport prints the enrichment summary as part of inrun validate output.
//
-// Example output for a Catalog with built-in and custom CRDs:
+// Example output for a Blueprint with built-in and custom CRDs:
//
// ✓ deployment-governance
// kind: Deployment → enriched from built-in registry
@@ -26,21 +26,21 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
+ "github.com/intentd/inrun/pkg/blueprint"
"github.com/intentd/inrun/pkg/types"
rbacv1 "k8s.io/api/rbac/v1"
)
// printCRDValidationLine prints one CRD entry's validation result.
// Shows enrichment clearly when it occurred, and any warnings.
-func printCRDValidationLine(cat *catalog.Catalog, entry types.CRDEntry) {
+func printCRDValidationLine(bp *blueprint.Blueprint, entry types.CRDEntry) {
printCRDHeader(entry)
- strictModeText := getStrictModeText(entry, cat.IsStrictModeEnabled())
+ strictModeText := getStrictModeText(entry, bp.IsStrictModeEnabled())
printKindInfo(entry)
printModeResync(entry)
- printProtectionStatus(entry, cat.IsDeletionProtectionEnabled(), strictModeText)
- printInfo(entry.Info, cat.Info)
- printWarnings(entry.Warnings, cat.Warnings)
+ printProtectionStatus(entry, bp.IsDeletionProtectionEnabled(), strictModeText)
+ printInfo(entry.Info, bp.Info)
+ printWarnings(entry.Warnings, bp.Warnings)
}
// printCRDHeader prints the CRD name with appropriate icon.
@@ -57,8 +57,8 @@ func printCRDHeader(entry types.CRDEntry) {
// getStrictModeText returns a formatted string indicating strict mode status,
// or an empty string if strict mode is not enforced for this CRD.
-func getStrictModeText(entry types.CRDEntry, catalogStrictMode bool) string {
- if catalogStrictMode && entry.IsStrictDeletionProtection(catalogStrictMode) {
+func getStrictModeText(entry types.CRDEntry, blueprintStrictMode bool) string {
+ if blueprintStrictMode && entry.IsStrictDeletionProtection(blueprintStrictMode) {
return cmdutil.InfoMark() + " (strict)"
}
return ""
@@ -101,13 +101,13 @@ func printModeResync(entry types.CRDEntry) {
// printProtectionStatus prints the protection level for custom CRDs.
// For built‑ins, a simplified message is shown.
// When strict mode is active, the label changes from "protection:" to "strict‑protection:".
-func printProtectionStatus(entry types.CRDEntry, catalogProtected bool, strictModeText string) {
+func printProtectionStatus(entry types.CRDEntry, blueprintProtected bool, strictModeText string) {
if entry.IsBuiltInType() {
fmt.Printf(" %s\n", cmdutil.Gray("protection: label-based (built-in)"))
return
}
- if !catalogProtected {
+ if !blueprintProtected {
return
}
@@ -141,9 +141,9 @@ func printProtectionStatus(entry types.CRDEntry, catalogProtected bool, strictMo
fmt.Printf(" %s\n", cmdutil.Gray(fullText))
}
-// printWarnings prints any warnings associated with the CRD or Catalog,
+// printWarnings prints any warnings associated with the CRD or Blueprint,
// with proper indentation.
-func printWarnings(crdWarnings, catalogWarnings []string) {
+func printWarnings(crdWarnings, blueprintWarnings []string) {
// Print CRD-level warnings
for _, w := range crdWarnings {
lines := strings.Split(w, "\n")
@@ -156,8 +156,8 @@ func printWarnings(crdWarnings, catalogWarnings []string) {
}
}
- // Print Catalog-level warnings
- for _, w := range catalogWarnings {
+ // Print Blueprint-level warnings
+ for _, w := range blueprintWarnings {
lines := strings.Split(w, "\n")
for i, line := range lines {
if i == 0 {
@@ -169,9 +169,9 @@ func printWarnings(crdWarnings, catalogWarnings []string) {
}
}
-// printInfo prints any informational messages associated with the CRD or Catalog,
+// printInfo prints any informational messages associated with the CRD or Blueprint,
// with proper indentation.
-func printInfo(crdInfo, catalogInfo []string) {
+func printInfo(crdInfo, blueprintInfo []string) {
// Print CRD-level info
for _, msg := range crdInfo {
lines := strings.Split(msg, "\n")
@@ -184,8 +184,8 @@ func printInfo(crdInfo, catalogInfo []string) {
}
}
- // Print Catalog-level info
- for _, msg := range catalogInfo {
+ // Print Blueprint-level info
+ for _, msg := range blueprintInfo {
lines := strings.Split(msg, "\n")
for i, line := range lines {
if i == 0 {
@@ -227,7 +227,7 @@ func printCRDPermissions(rules []rbacv1.PolicyRule) {
// printValidateDependencyGraph prints the startup-order dependency section for validate --full.
// Only called when there are dependencies (dd is never nil here).
-func printValidateDependencyGraph(dd *catalog.DependencyDisplay) {
+func printValidateDependencyGraph(dd *blueprint.DependencyDisplay) {
fmt.Println()
fmt.Println(cmdutil.Bold("startup order"))
maxName := 0
@@ -322,32 +322,32 @@ func rbacRes(r rbacv1.PolicyRule) string {
return res
}
-// ── Profiles display ─────────────────────────────────────────────────────────
+// ── Presets display ─────────────────────────────────────────────────────────
-type profileLine struct {
+type presetLine struct {
typLabel string
- profile string
+ preset string
location string
mixed bool
}
-// printCRDProfiles prints named profiles declared for a CRD under its header.
-// No-op when the CRD declares no profiles.
-func printCRDProfiles(entry types.CRDEntry) {
- lines := collectProfileLines(entry)
+// printCRDPresets prints named presets declared for a CRD under its header.
+// No-op when the CRD declares no presets.
+func printCRDPresets(entry types.CRDEntry) {
+ lines := collectPresetLines(entry)
if len(lines) == 0 {
return
}
- maxType, maxProfile := 0, 0
+ maxType, maxPreset := 0, 0
for _, l := range lines {
if n := len(l.typLabel); n > maxType {
maxType = n
}
- if n := len(l.profile); n > maxProfile {
- maxProfile = n
+ if n := len(l.preset); n > maxPreset {
+ maxPreset = n
}
}
- fmt.Printf(" %s\n", cmdutil.Gray("profiles:"))
+ fmt.Printf(" %s\n", cmdutil.Gray("presets:"))
for _, l := range lines {
suffix := ""
if l.mixed {
@@ -356,70 +356,70 @@ func printCRDProfiles(entry types.CRDEntry) {
fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf(
"%-*s %-*s %s%s",
maxType, l.typLabel,
- maxProfile, l.profile,
+ maxPreset, l.preset,
l.location,
suffix,
)))
}
}
-func collectProfileLines(entry types.CRDEntry) []profileLine {
- var lines []profileLine
+func collectPresetLines(entry types.CRDEntry) []presetLine {
+ var lines []presetLine
- for _, e := range entry.CollectSecurityProfileEntries() {
+ for _, e := range entry.CollectSecurityPresetEntries() {
label := "security (container)"
if e.Kind == "pod" {
label = "security (pod)"
}
- lines = append(lines, profileLine{
+ lines = append(lines, presetLine{
typLabel: label,
- profile: e.Profile,
- location: profileLoc(e.Resource, e.ResourceName, e.Phase),
+ preset: e.Preset,
+ location: presetLoc(e.Resource, e.ResourceName, e.Phase),
mixed: e.Mixed,
})
}
- for _, e := range entry.CollectResourceProfileEntries() {
- lines = append(lines, profileLine{
+ for _, e := range entry.CollectResourcePresetEntries() {
+ lines = append(lines, presetLine{
typLabel: "resources",
- profile: e.Profile,
- location: profileLoc(e.Resource, e.ResourceName, e.Phase),
+ preset: e.Preset,
+ location: presetLoc(e.Resource, e.ResourceName, e.Phase),
mixed: e.Mixed,
})
}
- for _, e := range entry.CollectHPAProfileEntries() {
- lines = append(lines, profileLine{
+ for _, e := range entry.CollectHPAPresetEntries() {
+ lines = append(lines, presetLine{
typLabel: "hpa",
- profile: e.Profile,
- location: profileLoc("", e.ResourceName, e.Phase),
+ preset: e.Preset,
+ location: presetLoc("", e.ResourceName, e.Phase),
mixed: e.Mixed,
})
}
- for _, e := range entry.CollectPDBProfileEntries() {
- lines = append(lines, profileLine{
+ for _, e := range entry.CollectPDBPresetEntries() {
+ lines = append(lines, presetLine{
typLabel: "pdb",
- profile: e.Profile,
- location: profileLoc("", e.ResourceName, e.Phase),
+ preset: e.Preset,
+ location: presetLoc("", e.ResourceName, e.Phase),
mixed: e.Mixed,
})
}
- for _, e := range entry.CollectRollingUpdateProfileEntries() {
- lines = append(lines, profileLine{
+ for _, e := range entry.CollectRollingUpdatePresetEntries() {
+ lines = append(lines, presetLine{
typLabel: "rolling",
- profile: e.Profile,
- location: profileLoc("", e.ResourceName, e.Phase),
+ preset: e.Preset,
+ location: presetLoc("", e.ResourceName, e.Phase),
mixed: e.Mixed,
})
}
- for _, e := range entry.CollectProbeProfileEntries() {
- lines = append(lines, profileLine{
+ for _, e := range entry.CollectProbePresetEntries() {
+ lines = append(lines, presetLine{
typLabel: "probes (" + e.ProbeType + ")",
- profile: e.Profile,
- location: profileLoc(e.Resource, e.ResourceName, e.Phase),
+ preset: e.Preset,
+ location: presetLoc(e.Resource, e.ResourceName, e.Phase),
mixed: e.Mixed,
})
}
@@ -427,8 +427,8 @@ func collectProfileLines(entry types.CRDEntry) []profileLine {
return lines
}
-// profileLoc builds a compact location string from resource kind, name, and phase.
-func profileLoc(resource, name, phase string) string {
+// presetLoc builds a compact location string from resource kind, name, and phase.
+func presetLoc(resource, name, phase string) string {
switch {
case resource != "" && name != "":
return resource + "/" + name + " [" + phase + "]"
diff --git a/cmd/cli/validate/simulate_spec_test.go b/cmd/cli/validate/simulate_spec_test.go
index 5d7e449d6..c1086d59c 100644
--- a/cmd/cli/validate/simulate_spec_test.go
+++ b/cmd/cli/validate/simulate_spec_test.go
@@ -12,13 +12,13 @@ import (
func writeSpec(t *testing.T, spec string) string {
t.Helper()
dir := t.TempDir()
- for _, f := range []string{"catalog.yaml", "intent.json", "cr.yaml"} {
+ for _, f := range []string{"blueprint.yaml", "intent.json", "cr.yaml"} {
if err := os.WriteFile(filepath.Join(dir, f), []byte("{}"), 0o600); err != nil {
t.Fatal(err)
}
}
p := filepath.Join(dir, "simulate.yaml")
- doc := "apiVersion: inrun.dev/v1\nkind: Simulate\nmetadata:\n name: s\nspec:\n catalog: catalog.yaml\n" + spec
+ doc := "apiVersion: inrun.dev/v1\nkind: Simulate\nmetadata:\n name: s\nspec:\n blueprint: blueprint.yaml\n" + spec
if err := os.WriteFile(p, []byte(doc), 0o600); err != nil {
t.Fatal(err)
}
diff --git a/cmd/cli/validate/template.go b/cmd/cli/validate/template.go
index 8d7d8e84e..d2f13afa4 100644
--- a/cmd/cli/validate/template.go
+++ b/cmd/cli/validate/template.go
@@ -10,14 +10,14 @@ import (
"github.com/intentd/inrun/cmd/cli/cmdutil"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
"github.com/intentd/inrun/pkg/types"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
-// templateRuntimeOutput is the serializable view of a fully-expanded Catalog.
+// templateRuntimeOutput is the serializable view of a fully-expanded Blueprint.
// It holds what the runtime receives after module expansion and validation.
type templateRuntimeOutput struct {
APIVersion string `yaml:"apiVersion" json:"apiVersion"`
@@ -32,8 +32,8 @@ type templateSpecOutput struct {
var templateCmd = &cobra.Command{
Use: "template",
- Short: "Render and inspect the fully-expanded runtime Catalog",
- Long: `Loads one or more Catalog or Stack files, fully expands all module imports,
+ Short: "Render and inspect the fully-expanded runtime Blueprint",
+ Long: `Loads one or more Blueprint or Stack files, fully expands all module imports,
resolves all template inputs, validates the configuration, and shows exactly
what the runtime will see — nothing more, nothing less.
@@ -41,12 +41,12 @@ By default prints a human-readable summary. Use flags to switch output format
or drill into a specific CRD.
Examples:
- inrun template -f catalog.yaml
- inrun template -f catalog.yaml --yaml
- inrun template -f catalog.yaml --json
- inrun template -f catalog.yaml --graph
- inrun template -f catalog.yaml --crd pipeline
- inrun template -f catalog.yaml --yaml -o runtime.yaml
+ inrun template -f blueprint.yaml
+ inrun template -f blueprint.yaml --yaml
+ inrun template -f blueprint.yaml --json
+ inrun template -f blueprint.yaml --graph
+ inrun template -f blueprint.yaml --crd pipeline
+ inrun template -f blueprint.yaml --yaml -o runtime.yaml
inrun template -f a.yaml -f b.yaml --graph`,
RunE: func(cmd *cobra.Command, args []string) error {
jsonOut, _ := cmd.Flags().GetBool("json")
@@ -58,29 +58,29 @@ Examples:
// ── Load & expand ───────────────────────────────────────────────────────
spin := cmdutil.StartSpinner("Resolving imports...")
- merged, err := cmdutil.GenerateCatalog(cmd)
+ merged, err := cmdutil.GenerateBlueprint(cmd)
if err != nil {
spin.Failure()
return err
}
spin.Stop()
- var cat *catalog.Catalog
+ var bp *blueprint.Blueprint
if noValidate {
- var raw catalog.Catalog
- if _, err = raw.BuildRuntimeCatalog(cmdutil.Cfg, merged.Merger); err != nil {
+ var raw blueprint.Blueprint
+ if _, err = raw.BuildRuntimeBlueprint(cmdutil.Cfg, merged.Merger); err != nil {
return err
}
- cat = &raw
+ bp = &raw
} else {
- cat, err = pipeline.BuildExpanded(cmdutil.Cfg, merged.Merger)
+ bp, err = pipeline.BuildExpanded(cmdutil.Cfg, merged.Merger)
if err != nil {
return fmt.Errorf("validation: %w", err)
}
}
- crds := cat.Enabled()
- depGraph := catalog.NewDependencyGraph(cat)
+ crds := bp.Enabled()
+ depGraph := blueprint.NewDependencyGraph(bp)
startupOrder := depGraph.StartupOrder()
// ── Route to output mode ────────────────────────────────────────────────
@@ -117,9 +117,9 @@ Examples:
cmdutil.PrintDependencyGraph(crds, depGraph, startupOrder)
return nil
- // ── --yaml / --json: full runtime Catalog ───────────────────────────────
+ // ── --yaml / --json: full runtime Blueprint ───────────────────────────────
case yamlOut || jsonOut || defaultOut:
- view := buildRuntimeView(cat)
+ view := buildRuntimeView(bp)
if jsonOut {
out, err = json.MarshalIndent(view, "", " ")
} else {
@@ -132,7 +132,7 @@ Examples:
// ── default: human-readable summary ────────────────────────────────────
default:
- cmdutil.PrintTemplateSummary(cat, crds, startupOrder)
+ cmdutil.PrintTemplateSummary(bp, crds, startupOrder)
return nil
}
@@ -158,9 +158,9 @@ Examples:
},
}
-// buildRuntimeView constructs the serializable runtime view from the expanded Catalog.
-func buildRuntimeView(cat *catalog.Catalog) templateRuntimeOutput {
- meta := cat.Metadata()
+// buildRuntimeView constructs the serializable runtime view from the expanded Blueprint.
+func buildRuntimeView(bp *blueprint.Blueprint) templateRuntimeOutput {
+ meta := bp.Metadata()
metaFields := map[string]string{}
if meta.Name != "" {
metaFields["name"] = meta.Name
@@ -179,11 +179,11 @@ func buildRuntimeView(cat *catalog.Catalog) templateRuntimeOutput {
}
return templateRuntimeOutput{
- APIVersion: cat.APIVersion,
- Kind: cat.Kind,
+ APIVersion: bp.APIVersion,
+ Kind: bp.Kind,
Metadata: metaFields,
Spec: templateSpecOutput{
- CRDs: cat.Enabled(),
+ CRDs: bp.Enabled(),
},
}
}
@@ -203,8 +203,8 @@ func pruneYamlFile(view templateRuntimeOutput) ([]byte, error) {
func init() {
cmdutil.RootCmd.AddCommand(templateCmd)
- templateCmd.Flags().BoolP("yaml", "y", false, "Output full expanded runtime Catalog as YAML")
- templateCmd.Flags().BoolP("json", "j", false, "Output full expanded runtime Catalog as JSON")
+ templateCmd.Flags().BoolP("yaml", "y", false, "Output full expanded runtime Blueprint as YAML")
+ templateCmd.Flags().BoolP("json", "j", false, "Output full expanded runtime Blueprint as JSON")
templateCmd.Flags().BoolP("graph", "g", false, "Show dependency graph with startup order")
templateCmd.Flags().StringP("crd", "c", "", "Drill into a specific CRD by name")
templateCmd.Flags().StringP("output", "o", "", "Write output to file instead of stdout")
diff --git a/cmd/cli/validate/validate.go b/cmd/cli/validate/validate.go
index ef1104874..55309e21f 100644
--- a/cmd/cli/validate/validate.go
+++ b/cmd/cli/validate/validate.go
@@ -12,8 +12,8 @@ import (
"path/filepath"
- "github.com/intentd/inrun/pkg/catalog"
- "github.com/intentd/inrun/pkg/catalog/pipeline"
+ "github.com/intentd/inrun/pkg/blueprint"
+ "github.com/intentd/inrun/pkg/blueprint/pipeline"
"github.com/intentd/inrun/pkg/config"
"github.com/intentd/inrun/pkg/registry/e2e"
modulepkg "github.com/intentd/inrun/pkg/registry/module"
@@ -25,17 +25,17 @@ import (
var validateCmd = &cobra.Command{
Use: "validate",
- Short: "Validate an Inrun pattern (Catalog, Stack, Module, E2E, Simulate)",
+ Short: "Validate an Inrun pattern (Blueprint, Stack, Module, E2E, Simulate)",
Long: `Validates any Inrun pattern and reports errors.
The pattern kind is detected automatically from the 'kind' field:
- Catalog — operator definition with CRD declarations
- Stack — multi-source catalog composer
+ Blueprint — operator definition with CRD declarations
+ Stack — multi-source blueprint composer
Module — reusable operator pattern
E2E — declarative end-to-end test spec
Simulate — declarative reconciler assertions
-Reads catalog.yaml or stack.yaml from the current directory by default.
+Reads blueprint.yaml or stack.yaml from the current directory by default.
Pass -f to validate a different file.
Examples:
@@ -50,7 +50,7 @@ Examples:
expanded = cmdutil.DefaultFilePaths()
}
if len(expanded) == 0 {
- return fmt.Errorf(cmdutil.ErrNoCatalog)
+ return fmt.Errorf(cmdutil.ErrNoBlueprint)
}
var docKind string
@@ -89,52 +89,52 @@ Examples:
docKind = kind
}
- // Default path: Catalog / Stack validation
+ // Default path: Blueprint / Stack validation
spin := cmdutil.StartSpinner("Resolving imports...")
- m, err := cmdutil.GenerateCatalog(cmd)
+ m, err := cmdutil.GenerateBlueprint(cmd)
if err != nil {
spin.Failure()
return err
}
spin.Stop()
- cat, err := pipeline.BuildExpanded(cmdutil.Cfg, m.Merger)
+ bp, err := pipeline.BuildExpanded(cmdutil.Cfg, m.Merger)
if err != nil {
- var typedErr *catalog.TypedOperatorError
+ var typedErr *blueprint.TypedOperatorError
if errors.As(err, &typedErr) {
cmdutil.PrintTypedOperatorHint(typedErr, "inrun validate")
}
return err
}
- entries := cat.EnabledCRDs()
+ entries := bp.EnabledCRDs()
- kindLabel := "Catalog"
+ kindLabel := "Blueprint"
if config.IsStackKind(docKind) {
kindLabel = "Stack"
}
- if cat.IsStandaloneGateway() {
+ if bp.IsStandaloneGateway() {
kindLabel = "Gateway Standalone"
}
full, _ := cmd.Flags().GetBool("full")
- showNotes, _ := cmd.Flags().GetBool("notes")
- showProfiles, _ := cmd.Flags().GetBool("profiles")
+ showFunctions, _ := cmd.Flags().GetBool("functions")
+ showPresets, _ := cmd.Flags().GetBool("presets")
- // --notes / --profiles: quiet mode — skip full validate output, print only that registry.
- if showNotes || showProfiles {
- if showNotes {
- printValidateNotes(cat.Notes)
+ // --functions / --presets: quiet mode — skip full validate output, print only that registry.
+ if showFunctions || showPresets {
+ if showFunctions {
+ printValidateFunctions(bp.Functions)
}
- if showProfiles {
- printValidateProfiles(cat.Profiles)
+ if showPresets {
+ printValidatePresets(bp.Presets)
}
return nil
}
var perCRDPerms map[string][]rbacv1.PolicyRule
if full {
- perCRDPerms = cat.GeneratePerCRDRBACRules()
+ perCRDPerms = bp.GeneratePerCRDRBACRules()
}
// Sort entries by name for stable output across runs.
@@ -157,14 +157,14 @@ Examples:
if config.IsStackKind(docKind) {
deprecationHint = "To acknowledge this import, add it to lifecycle.accept.patterns in your Stack."
}
- cmdutil.PrintCatalogDeprecationWithHint(cat.Deprecation(), deprecationHint)
+ cmdutil.PrintBlueprintDeprecationWithHint(bp.Deprecation(), deprecationHint)
// Print each CRD entry with enrichment info
for _, entry := range sortedEntries {
- printCRDValidationLine(cat, entry)
+ printCRDValidationLine(bp, entry)
if full {
printCRDPermissions(perCRDPerms[entry.Name])
- printCRDProfiles(entry)
+ printCRDPresets(entry)
}
fmt.Println()
@@ -184,23 +184,23 @@ Examples:
fmt.Println(strings.Repeat("─", 60))
fmt.Printf("%d %s valid (%d built-in, %d custom)\n", len(entries), crdText, builtIn, custom)
- for _, w := range cat.CronConditionWarnings() {
+ for _, w := range bp.CronConditionWarnings() {
fmt.Printf("\n%s %s\n", cmdutil.Yellow("⚠"), w)
}
if full {
- if dd := cat.DependencyDisplayData(); dd != nil {
+ if dd := bp.DependencyDisplayData(); dd != nil {
printValidateDependencyGraph(dd)
}
- printRuntimePermissionsSection(cat.GenerateRuntimeRBACRules())
- printGatewayPermissionsSection(cat.GenerateGatewayRBACRules())
+ printRuntimePermissionsSection(bp.GenerateRuntimeRBACRules())
+ printGatewayPermissionsSection(bp.GenerateGatewayRBACRules())
printRuntimeContext()
fmt.Println()
}
// Always print non-empty registries inline.
- printValidateNotes(cat.Notes)
- printValidateProfiles(cat.Profiles)
+ printValidateFunctions(bp.Functions)
+ printValidatePresets(bp.Presets)
return nil
},
@@ -238,7 +238,7 @@ func ValidateE2EFile(path string) error {
}
baseDir := filepath.Dir(path)
- isAggregator := len(doc.Imports) > 0 && doc.Spec.Catalog == "" && doc.Spec.Init == nil
+ isAggregator := len(doc.Imports) > 0 && doc.Spec.Blueprint == "" && doc.Spec.Init == nil
var errs []string
@@ -261,8 +261,8 @@ func ValidateE2EFile(path string) error {
}
expanded := doc.Spec.Expect
if !isAggregator {
- if doc.Spec.Catalog == "" && doc.Spec.Init == nil && !isCustom {
- errs = append(errs, "spec.catalog is required (or spec.init for example packs, spec.custom.target for custom targets, or imports)")
+ if doc.Spec.Blueprint == "" && doc.Spec.Init == nil && !isCustom {
+ errs = append(errs, "spec.blueprint is required (or spec.init for example packs, spec.custom.target for custom targets, or imports)")
}
if doc.Spec.CRD == "" && doc.Spec.Init == nil && !isCustom {
errs = append(errs, "spec.crd is required (or spec.init for example packs, spec.custom.target for custom targets, or imports)")
@@ -339,8 +339,8 @@ func ValidateE2EFile(path string) error {
if isCustom {
fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("mode : custom target (%s — Inrun install skipped)", doc.Spec.Custom.Target)))
}
- if doc.Spec.Catalog != "" {
- fmt.Printf(" %s\n", cmdutil.Gray("catalog : "+doc.Spec.Catalog))
+ if doc.Spec.Blueprint != "" {
+ fmt.Printf(" %s\n", cmdutil.Gray("blueprint : "+doc.Spec.Blueprint))
}
if doc.Spec.CRD != "" {
fmt.Printf(" %s\n", cmdutil.Gray("crd : "+doc.Spec.CRD))
@@ -456,10 +456,10 @@ func validateSimulateFileOpts(path string, quiet bool) error {
}
}
if doc.Spec != nil {
- if doc.Spec.Catalog == "" {
- errs = append(errs, "spec.catalog is required")
- } else if !cmdutil.FileExists(filepath.Join(baseDir, doc.Spec.Catalog)) {
- errs = append(errs, "spec.catalog not found: "+doc.Spec.Catalog)
+ if doc.Spec.Blueprint == "" {
+ errs = append(errs, "spec.blueprint is required")
+ } else if !cmdutil.FileExists(filepath.Join(baseDir, doc.Spec.Blueprint)) {
+ errs = append(errs, "spec.blueprint not found: "+doc.Spec.Blueprint)
}
switch {
case doc.Spec.Intent != "" && len(doc.Spec.AllCRPaths()) > 0:
@@ -546,7 +546,7 @@ func validateSimulateFileOpts(path string, quiet bool) error {
return nil
}
- // Success — print structured summary matching the Catalog/E2E style.
+ // Success — print structured summary matching the Blueprint/E2E style.
fmt.Printf("%s %s\n", cmdutil.HealthIcon("ready"), cmdutil.Bold(doc.Metadata.Name))
if doc.Metadata.Description != "" {
fmt.Printf(" %s\n", cmdutil.Gray(doc.Metadata.Description))
@@ -563,7 +563,7 @@ func validateSimulateFileOpts(path string, quiet bool) error {
if cycles <= 0 {
cycles = 10
}
- fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("catalog : %s", doc.Spec.Catalog)))
+ fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("blueprint : %s", doc.Spec.Blueprint)))
if doc.Spec.Intent != "" {
fmt.Printf(" %s\n", cmdutil.Gray(fmt.Sprintf("intent : %s (token %s)", doc.Spec.Intent, doc.Spec.Token)))
} else {
@@ -615,7 +615,7 @@ func validateModuleFile(path string) error {
fmt.Println(cmdutil.Bold("Validating Module..."))
fmt.Println()
- errs := catalog.ValidateModule(path)
+ errs := blueprint.ValidateModule(path)
if len(errs) > 0 {
for _, e := range errs {
fmt.Printf(" %s %s\n", cmdutil.FailureMark(), e.Error())
@@ -648,8 +648,8 @@ func validateModuleFile(path string) error {
if summary := moduleResourceSummary(m); summary != "" {
fmt.Printf(" %s\n", cmdutil.Gray("resources: "+summary))
}
- if summary := moduleProfileSummary(m); summary != "" {
- fmt.Printf(" %s\n", cmdutil.Gray("profiles : "+summary))
+ if summary := modulePresetSummary(m); summary != "" {
+ fmt.Printf(" %s\n", cmdutil.Gray("presets : "+summary))
}
}
@@ -659,10 +659,10 @@ func validateModuleFile(path string) error {
return nil
}
-// moduleProfileSummary returns a compact string listing non-empty profile classes
+// modulePresetSummary returns a compact string listing non-empty preset classes
// and their counts, e.g. "networkPolicies(2) resourceQuotas(1)".
-func moduleProfileSummary(m *types.Module) string {
- reg := m.Profiles
+func modulePresetSummary(m *types.Module) string {
+ reg := m.Presets
if reg.Empty() {
return ""
}
@@ -675,9 +675,9 @@ func moduleProfileSummary(m *types.Module) string {
add("networkPolicies", len(reg.NetworkPolicies))
add("resourceQuotas", len(reg.ResourceQuotas))
add("limitRanges", len(reg.LimitRanges))
- add("hpa", len(reg.HPA))
- add("pdb", len(reg.PDB))
- add("rollingUpdate", len(reg.RollingUpdate))
+ add("hpa", len(reg.HPAs))
+ add("pdb", len(reg.PDBs))
+ add("rollingUpdate", len(reg.RollingUpdates))
return strings.Join(parts, " ")
}
@@ -718,7 +718,7 @@ func moduleResourceSummary(m *types.Module) string {
return strings.Join(parts, " ")
}
-func printValidateProfiles(reg types.ProfileRegistry) {
+func printValidatePresets(reg types.PresetRegistry) {
if reg.Empty() {
return
}
@@ -727,19 +727,19 @@ func printValidateProfiles(reg types.ProfileRegistry) {
for _, p := range reg.Resources {
entries = append(entries, entry{"resources", p.Name})
}
- for _, p := range reg.ContainerSecurity {
+ for _, p := range reg.ContainerSecurities {
entries = append(entries, entry{"containerSecurity", p.Name})
}
- for _, p := range reg.PodSecurity {
+ for _, p := range reg.PodSecurities {
entries = append(entries, entry{"podSecurity", p.Name})
}
- for _, p := range reg.HPA {
+ for _, p := range reg.HPAs {
entries = append(entries, entry{"hpa", p.Name})
}
- for _, p := range reg.PDB {
+ for _, p := range reg.PDBs {
entries = append(entries, entry{"pdb", p.Name})
}
- for _, p := range reg.RollingUpdate {
+ for _, p := range reg.RollingUpdates {
entries = append(entries, entry{"rollingUpdate", p.Name})
}
for _, p := range reg.Reconciler {
@@ -764,7 +764,7 @@ func printValidateProfiles(reg types.ProfileRegistry) {
}
}
fmt.Println()
- fmt.Printf("%s\n", cmdutil.Bold(fmt.Sprintf("Profiles (%d)", len(entries))))
+ fmt.Printf("%s\n", cmdutil.Bold(fmt.Sprintf("Presets (%d)", len(entries))))
for _, e := range entries {
fmt.Printf(" %s %s\n", cmdutil.PadRight(cmdutil.Cyan(e.kind), maxLen), cmdutil.Gray(e.name))
}
@@ -778,19 +778,19 @@ func printRuntimeContext() {
fmt.Printf(" %s %s\n", cmdutil.PadRight(cmdutil.Cyan(".inrun.version"), 16), cmdutil.Gray("running Inrun version string"))
}
-func printValidateNotes(reg types.NoteRegistry) {
+func printValidateFunctions(reg types.KELFunctionRegistry) {
if reg.Empty() {
return
}
maxLen := 0
- for _, n := range reg.Functions {
+ for _, n := range reg.Definitions {
if l := len(n.Name); l > maxLen {
maxLen = l
}
}
fmt.Println()
- fmt.Printf("%s\n", cmdutil.Bold(fmt.Sprintf("Notes (%d)", len(reg.Functions))))
- for _, n := range reg.Functions {
+ fmt.Printf("%s\n", cmdutil.Bold(fmt.Sprintf("Functions (%d)", len(reg.Definitions))))
+ for _, n := range reg.Definitions {
fmt.Printf(" %s %s\n", cmdutil.PadRight(cmdutil.Cyan(n.Name), maxLen), cmdutil.Gray(n.Expression))
}
}
@@ -800,8 +800,8 @@ func init() {
validateCmd.Flags().StringSliceP("file", "f", nil, "Path to an Inrun pattern (repeatable or comma-separated)")
validateCmd.Flags().Bool("full", false, "Show per-CRD permissions, dependency graph, and system-level RBAC")
- validateCmd.Flags().Bool("notes", false, "Quiet mode: print only the merged note registry, skip full validate output")
- validateCmd.Flags().Bool("profiles", false, "Quiet mode: print only the merged profile registry, skip full validate output")
+ validateCmd.Flags().Bool("functions", false, "Quiet mode: print only the merged function registry, skip full validate output")
+ validateCmd.Flags().Bool("presets", false, "Quiet mode: print only the merged preset registry, skip full validate output")
// Shadow global flags so they don't appear under `inrun validate`
cmdutil.ShadowGlobalCommandFlags(validateCmd)
diff --git a/cmd/console/CONTRIBUTING.md b/cmd/console/CONTRIBUTING.md
index 3b303af4b..c6bb90a69 100644
--- a/cmd/console/CONTRIBUTING.md
+++ b/cmd/console/CONTRIBUTING.md
@@ -36,7 +36,7 @@ We're actively looking for contributions in these areas:
- [ ] **Mobile responsive improvements**
#### Nice to Have
-- [ ] **Catalog diff view** – Compare Catalogs across environments
+- [ ] **Blueprint diff view** – Compare Blueprints across environments
- [ ] **Audit logging** – Track who viewed what and when
- [ ] **Bookmarkable state** – Save filter/search in URL
- [ ] **Keyboard shortcuts** – Power user navigation
@@ -107,11 +107,11 @@ When modifying HTML templates:
```bash
# Terminal 1 – Inrun Runtime 1
export INRUN_PORT=8080
-inrun -f catalog1.yaml
+inrun -f blueprint1.yaml
# Terminal 2 – Inrun Runtime 2
export INRUN_PORT=8081
-inrun -f catalog2.yaml
+inrun -f blueprint2.yaml
# Terminal 3 – Console
cd console
diff --git a/cmd/console/main.go b/cmd/console/main.go
index af0218db2..9921f304d 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -1,4 +1,4 @@
-// Command console is the console: a web UI that shows the Catalogs,
+// Command console is the console: a web UI that shows the Blueprints,
// CRDs and custom resources of one or more running runtimes, and lets
// developers submit intents through the gateway's generated forms. It reads
// everything over the runtime and gateway HTTP APIs and has no Kubernetes
@@ -40,7 +40,7 @@ func main() {
inrunURLs = flag.String("u", strings.Join(cfg.URLs, ","), "Comma-separated URLs of Inrun runtime instances")
ignoreDefault = flag.Bool("ignore-default", cfg.IgnoreDefault, "Do not add the default localhost:8080 URL; start with no instances")
port = flag.String("p", cfg.Port, "Port to serve the console on")
- refresh = flag.Duration("refresh", cfg.RefreshInterval, "Refresh interval for fetching Catalogs")
+ refresh = flag.Duration("refresh", cfg.RefreshInterval, "Refresh interval for fetching Blueprints")
logLevel = flag.String("log-level", cfg.LogLevel, "Log level (debug, info, warn, error)")
showVersion = flag.Bool("version", false, "Show version information")
)
diff --git a/cmd/console/web/assets/static/css/style.css b/cmd/console/web/assets/static/css/style.css
index bcad26d99..e5822430e 100644
--- a/cmd/console/web/assets/static/css/style.css
+++ b/cmd/console/web/assets/static/css/style.css
@@ -347,7 +347,7 @@ code, .mono {
color: var(--text-secondary);
}
-/* ── List view for CRD cards (catalog.html) ────────────────── */
+/* ── List view for CRD cards (blueprint.html) ────────────────── */
.console-card-grid.list-view {
display: flex;
flex-direction: column;
@@ -1150,7 +1150,7 @@ code, .mono {
overflow: hidden;
}
-/* ── Info Card (catalog info) ──────────────────────────────── */
+/* ── Info Card (blueprint info) ──────────────────────────────── */
.console-info-card {
background: var(--bg-surface);
border: 1px solid var(--border);
@@ -1762,7 +1762,7 @@ details > summary::-webkit-details-marker { display: none; }
}
}
-/* ── CRD / Catalog config popup ─────────────────────────────── */
+/* ── CRD / Blueprint config popup ─────────────────────────────── */
.console-crumb-clickable {
cursor: pointer;
padding: 2px 6px;
@@ -1774,7 +1774,7 @@ details > summary::-webkit-details-marker { display: none; }
color: var(--accent);
}
-.console-catalog-name-link:hover svg {
+.console-blueprint-name-link:hover svg {
opacity: 0.8 !important;
}
diff --git a/cmd/console/web/assets/static/inrun-mark.svg b/cmd/console/web/assets/static/inrun-mark.svg
index d982ea3d5..a25a0de2e 100644
--- a/cmd/console/web/assets/static/inrun-mark.svg
+++ b/cmd/console/web/assets/static/inrun-mark.svg
@@ -1,5 +1,23 @@
diff --git a/cmd/console/web/assets/static/inrun-wordmark.svg b/cmd/console/web/assets/static/inrun-wordmark.svg
index becc3ccb9..c0cdf390e 100644
--- a/cmd/console/web/assets/static/inrun-wordmark.svg
+++ b/cmd/console/web/assets/static/inrun-wordmark.svg
@@ -1,6 +1,25 @@
-