Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
31fde75
chore(s10): add OpenShift Local plan; mark S9 done
patterncatalyst Oct 11, 2026
a0f03c4
feat(demo-05): propagate W3C trace context through Kafka headers in t…
patterncatalyst Oct 11, 2026
904a236
chore(s10): record steps 1-2 in the plan
patterncatalyst Oct 11, 2026
8e2992a
refactor(demo-05): share the image build table between the minikube a…
patterncatalyst Oct 11, 2026
9b19ce3
feat(demo-05): OpenShift manifests (operators, infra, five language o…
patterncatalyst Oct 11, 2026
7646ef7
feat(demo-05): OpenShift Local scripts (setup, build and push, deploy…
patterncatalyst Oct 11, 2026
de36779
docs(demo-05): note the OpenShift invocation in verify.sh
patterncatalyst Oct 11, 2026
f62dc44
ci: validate OpenShift manifests and select openshift/ with k8s/
patterncatalyst Oct 11, 2026
d059628
docs(appendix-s): topology and image-flow diagrams
patterncatalyst Oct 11, 2026
eb0d76e
docs(appendix-s): Appendix S, index, README, prerequisites, chapter 0…
patterncatalyst Oct 11, 2026
a379989
fix(demo-05): keep OpenShift inventory from failing on a clean cluster
patterncatalyst Oct 11, 2026
1f77342
fix(demo-05): wait for OpenShift operators to settle after the regist…
patterncatalyst Oct 11, 2026
82fdd88
fix(demo-05): ignore OLM-copied CSVs in the OpenShift operator checks
patterncatalyst Oct 11, 2026
f007b5b
docs(appendix-s): record live OpenShift Local results
patterncatalyst Oct 11, 2026
4f15a38
fix(demo-05): wait for a stable cluster and retry applies after a col…
patterncatalyst Oct 11, 2026
b932bb4
fix(demo-05): remove only CRDs owned by our CSV and always stop CRC i…
patterncatalyst Oct 11, 2026
d379ed7
fix(ci): a script rendered into a manifest selects the manifest direc…
patterncatalyst Oct 11, 2026
a2e7bdf
docs(appendix-s): accurate teardown scope, crc-admin context on every…
patterncatalyst Oct 11, 2026
f410df1
fix(demo-05): .NET consumers skip null header values and spans record…
patterncatalyst Oct 11, 2026
71d0ca9
docs(appendix-s): record repair round 1 in the S10 plan
patterncatalyst Oct 11, 2026
94ac394
fix(demo-05): find kept CRDs by their OLM label and fail fast without…
patterncatalyst Oct 11, 2026
0cdec8b
docs(appendix-s): describe label-based CRD cleanup
patterncatalyst Oct 11, 2026
0c874e7
fix(demo-05): refuse non-OLM Strimzi, remove the OLM Operator object,…
patterncatalyst Oct 12, 2026
e20cdc4
chore(s10): record final live verification
patterncatalyst Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 59 additions & 20 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -498,17 +498,18 @@ jobs:
done < <(jq -r '.[]' <<<"$FILES")
exit "$failed"

# Kubernetes manifests: render the infrastructure and every language overlay, validate
# against the 1.36.5 schemas (and the Strimzi CRDs), and assert the conventions the
# minikube run relies on. No cluster is needed.
# Kubernetes and OpenShift manifests: render the infrastructure and every language overlay, validate
# against the Kubernetes schemas (and the Strimzi, OLM and OpenShift ones), and assert the conventions
# the minikube (k8s/) and OpenShift Local (openshift/) runs rely on. No cluster is needed.
# The selector picks examples/<NN>/k8s and examples/<NN>/openshift; a k8s/ change picks both.
k8s:
name: Kubernetes manifests
name: Kubernetes and OpenShift manifests
needs: changes
if: needs.changes.outputs.k8s != '[]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The runner's kubectl is not pinned; install the version the cluster runs.
# The runner's kubectl is not pinned; install the version the minikube cluster runs.
- name: Install kubectl v1.36.5 and kubeconform v0.8.0
env:
KUBECTL_SHA256: 33bc88a24c3b09cf55bfd59e6ca977881b61fc3f25ab65bc80c65b430248648e
Expand All @@ -526,32 +527,70 @@ jobs:
- name: Render, validate and assert
env:
DIRS: ${{ needs.changes.outputs.k8s }}
# datreeio/CRDs-catalog pinned to a commit; it carries the Strimzi schemas.
# OpenShift 4.22 runs Kubernetes 1.35. To confirm against `oc version` in the live run.
OCP_KUBE_VERSION: 1.35.6
# datreeio/CRDs-catalog pinned to a commit; it carries the Strimzi and OLM schemas and, under
# openshift/v4.15-strict, the Route and ImageStream ones (a different file name layout).
CRDS: https://raw.githubusercontent.com/datreeio/CRDs-catalog/63669a570e231d4f1f8396d229a1de512bcf0a34/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
OCP_CRDS: https://raw.githubusercontent.com/datreeio/CRDs-catalog/63669a570e231d4f1f8396d229a1de512bcf0a34/openshift/v4.15-strict/{{.ResourceKind}}_{{.Group}}_{{.ResourceAPIVersion}}.json
run: |
failed=0
while IFS= read -r dir; do
[ -d "$dir" ] || { echo "::error::$dir not found"; failed=1; continue; }
kubeconform -v
kubeconform -strict -summary -kubernetes-version 1.36.5 -schema-location default -schema-location "$CRDS" "$dir/namespace.yaml" || failed=1
targets=("$dir/infra")
flavor="$(basename "$dir")"
if [ "$flavor" = openshift ]; then
kver="$OCP_KUBE_VERSION"
targets=("$dir/operators" "$dir/infra")
# Exactly the five languages that run on OpenShift Local (no C++), none of them extra.
langs="$(for o in "$dir"/overlays/*/; do basename "$o"; done | sort | tr '\n' ' ')"
[ "$langs" = "dotnet go python quarkus spring-boot " ] || { echo "::error::$dir/overlays must be dotnet go python quarkus spring-boot, found: $langs"; failed=1; }
# Credentials and cluster access: no token or password handling the plan forbids, in code (comments excluded).
if grep -rhvE '^\s*#' "$dir"/*.sh | grep -nE 'use-context|oc adm policy|port-forward|skopeo login|--dest-creds|--dest-registry-token|--tls-verify=false|--insecure|curl[^|]* -k |crc delete'; then
echo "::error::$dir scripts use a forbidden command"; failed=1
fi
else
kver=1.36.5
kubeconform -strict -summary -kubernetes-version "$kver" -schema-location default -schema-location "$CRDS" "$dir/namespace.yaml" || failed=1
targets=("$dir/infra")
fi
for o in "$dir"/overlays/*/; do targets+=("${o%/}"); done
for t in "${targets[@]}"; do
echo "::group::$t"
# infra reads the shared schema from ../../db/init, outside its directory.
# infra reads the shared schema from ../../db/init and openshift/infra the k8s files, outside its directory.
kubectl kustomize --load-restrictor LoadRestrictionsNone "$t" > "$RUNNER_TEMP/render.yaml" || { echo "::error::render failed: $t"; failed=1; echo "::endgroup::"; continue; }
kubeconform -strict -summary -kubernetes-version 1.36.5 -schema-location default -schema-location "$CRDS" "$RUNNER_TEMP/render.yaml" || { echo "::error::kubeconform failed: $t"; failed=1; }
kubeconform -strict -summary -kubernetes-version "$kver" -schema-location default -schema-location "$CRDS" -schema-location "$OCP_CRDS" "$RUNNER_TEMP/render.yaml" || { echo "::error::kubeconform failed: $t"; failed=1; }
if grep -nE 'image: .*:latest|:latest$' "$RUNNER_TEMP/render.yaml"; then echo "::error::$t uses :latest"; failed=1; fi
case "$t" in
*/overlays/*)
lang="$(basename "$t")"
[ "$(grep -cE "image: localhost/cndp-05/${lang}-[a-z-]+:v1$" "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must use 3 images localhost/cndp-05/${lang}-<svc>:v1"; failed=1; }
[ "$(grep -c 'imagePullPolicy: Never' "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must set imagePullPolicy: Never on 3 containers"; failed=1; }
grep -q 'nodePort: 30080' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t lacks nodePort 30080"; failed=1; } ;;
*/infra)
grep -q 'nodePort: 30300' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t lacks nodePort 30300"; failed=1; }
grep -vE 'image: (docker.io|quay.io)/' <(grep -E '^\s+image: ' "$RUNNER_TEMP/render.yaml") && { echo "::error::$t has an image that is not fully qualified"; failed=1; } ;;
esac
if [ "$flavor" = openshift ]; then
if grep -nE 'nodePort|runAsUser|fsGroup|anyuid' "$RUNNER_TEMP/render.yaml"; then echo "::error::$t has a nodePort, a fixed UID or group, or anyuid"; failed=1; fi
case "$t" in
*/operators)
grep -q 'installPlanApproval: Manual' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t Subscription must use installPlanApproval: Manual"; failed=1; }
grep -qE 'startingCSV: amqstreams\.v[0-9.]+-[0-9]+' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t Subscription must pin startingCSV"; failed=1; }
grep -q 'targetNamespaces' "$RUNNER_TEMP/render.yaml" && { echo "::error::$t OperatorGroup must be AllNamespaces"; failed=1; } ;;
*/overlays/*)
lang="$(basename "$t")"
reg=image-registry.openshift-image-registry.svc:5000/cndp-05
[ "$(grep -cE "image: ${reg}/${lang}-[a-z-]+:v1$" "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must use 3 images ${reg}/${lang}-<svc>:v1"; failed=1; }
[ "$(grep -c 'imagePullPolicy: Always' "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must set imagePullPolicy: Always on 3 containers"; failed=1; }
if ! { grep -q 'kind: Route' "$RUNNER_TEMP/render.yaml" && grep -q 'termination: edge' "$RUNNER_TEMP/render.yaml" && grep -q 'insecureEdgeTerminationPolicy: Redirect' "$RUNNER_TEMP/render.yaml"; }; then echo "::error::$t lacks the edge Route with Redirect"; failed=1; fi ;;
*/infra)
if ! { grep -q 'kind: Route' "$RUNNER_TEMP/render.yaml" && grep -q 'insecureEdgeTerminationPolicy: Redirect' "$RUNNER_TEMP/render.yaml"; }; then echo "::error::$t lacks the Grafana Route"; failed=1; fi
grep -q 'version: 4.2.0' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t Kafka must run version 4.2.0 (the AMQ Streams bundle)"; failed=1; }
grep -vE 'image: (docker.io|quay.io|registry.redhat.io)/' <(grep -E '^\s+image: ' "$RUNNER_TEMP/render.yaml") && { echo "::error::$t has an image that is not fully qualified"; failed=1; } ;;
esac
else
case "$t" in
*/overlays/*)
lang="$(basename "$t")"
[ "$(grep -cE "image: localhost/cndp-05/${lang}-[a-z-]+:v1$" "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must use 3 images localhost/cndp-05/${lang}-<svc>:v1"; failed=1; }
[ "$(grep -c 'imagePullPolicy: Never' "$RUNNER_TEMP/render.yaml")" -eq 3 ] || { echo "::error::$t must set imagePullPolicy: Never on 3 containers"; failed=1; }
grep -q 'nodePort: 30080' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t lacks nodePort 30080"; failed=1; } ;;
*/infra)
grep -q 'nodePort: 30300' "$RUNNER_TEMP/render.yaml" || { echo "::error::$t lacks nodePort 30300"; failed=1; }
grep -vE 'image: (docker.io|quay.io)/' <(grep -E '^\s+image: ' "$RUNNER_TEMP/render.yaml") && { echo "::error::$t has an image that is not fully qualified"; failed=1; } ;;
esac
fi
echo "::endgroup::"
done
done < <(jq -r '.[]' <<<"$DIRS")
Expand Down
15 changes: 12 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,18 @@ Example 05 also runs on minikube (`examples/05-event-driven/k8s/`, Appendix R):
(docker driver, containerd, Kubernetes v1.36.5), NodePorts 30080 and 30300 published on 127.0.0.1 at
creation, Strimzi 1.2.0 for Kafka, kustomize manifests, `RUNTIME=kubernetes ./verify.sh`. Always pass
`--keep-context` to `minikube start` (setup-profile.sh also restores the prior current-context, because minikube's restart path overrides the flag) and `--context cndp-k8s` to kubectl; never use port-forward,
`minikube tunnel` or `minikube service`. OpenShift Local (Appendix S) is added later in
the modernization plan. Prometheus/Mimir is published on host port 19090 (Cockpit holds 9090
on Fedora/RHEL). Each example must run standalone.
`minikube tunnel` or `minikube service`. Prometheus/Mimir is published on host port 19090 (Cockpit holds 9090
on Fedora/RHEL). Each example must run standalone. Example 05 also runs on OpenShift Local
(`examples/05-event-driven/openshift/`, Appendix S; Spring Boot, Quarkus, .NET, Python, Go): crc 2.64.0 sized
12 vCPU / 32768 MiB, AMQ Streams from OLM (pinned `startingCSV`, `installPlanApproval: Manual`, only that
InstallPlan approved), Kafka 4.2.0, `restricted-v2` pods with no fixed UID, Red Hat `rhel10/postgresql-18`,
edge-TLS Routes verified with the ingress CA, `RUNTIME=kubernetes KUBECTL=oc ./verify.sh`. OpenShift rules:
call `oc --context crc-admin` on every command and never `oc config use-context` (`setup-crc.sh` restores the
recorded current-context, including "none"); never print or store the kubeadmin password, a token or the pull
secret; images go to the internal registry with `skopeo copy` and a `builder` token held in a shell variable and
passed through a process-substitution authfile (no `skopeo login`, no `--dest-creds`, no token in a file or an
argument list); never `curl -k` or port-forward; CRC is stopped after use, never deleted, and `teardown.sh`
removes only the CRDs our CSV owns or that carry OLM's `operators.coreos.com/amq-streams.cndp-amq-streams` label (and only with no foreign Kafka items), plus OLM's `Operator` object for that package; it reports foreign items and never removes them.

## Running-example domain

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ the *Designing Cloud-Native APIs* deck series; the decks themselves live in

## What's inside

- **A setup guide, 13 chapters, 18 deep-dive appendices (A–R), and a glossary** —
- **A setup guide, 13 chapters, 19 deep-dive appendices (A–S), and a glossary** —
covering REST, gRPC, GraphQL, WebSockets, event-driven architecture, schemas and
versioning, observability, resilience, and the patterns that tie them together.
- **Six languages, one set of tabs** — every code sample appears in Spring Boot,
Expand Down Expand Up @@ -55,7 +55,7 @@ cd python && docker compose down -v
| Path | What it holds |
|---|---|
| `_parts/` | the five Parts that form the homepage cards |
| `_docs/` | the setup guide, chapters, appendices A–R, and the glossary |
| `_docs/` | the setup guide, chapters, appendices A–S, and the glossary |
| `_includes/codetabs.html` + `assets/js/codetabs.js` | the multi-language code tabs (no plugin) |
| `assets/diagrams/` | paired `name.svg` + `name.excalidraw` figures |
| `lgtm-presentation/` | the six source decks + `BUILD.md` manifest |
Expand Down
4 changes: 2 additions & 2 deletions _docs/05-event-driven.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,8 +1291,8 @@ redelivery is the at-least-once guarantee, and the reason your handler dedupes.

The code is in [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/). The run script there builds and runs
it; its `README.md` covers what it does and how to drive it. The same example, with Kafka managed by
the Strimzi operator, also runs on a Kubernetes cluster; Appendix R covers that deployment.
the Strimzi operator, also runs on a Kubernetes cluster; Appendix R covers that deployment on minikube and Appendix S covers OpenShift Local.

---
*Verification status: verified — [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10) and on minikube (2026-10-11, Appendix R)
*Verification status: verified — [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10) and on minikube (2026-10-11, Appendix R), and 10/10 in five languages on OpenShift Local 4.22.14 (2026-10-11, Appendix S)
(event publication to order.placed, fan-out to two independent consumer groups (shipping and notification), and idempotent consumers under a redelivered duplicate event).*
Loading
Loading