diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index a6f14cb0..81ece871 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -35,6 +35,7 @@ jobs: cpp: ${{ steps.select.outputs.cpp }} compose: ${{ steps.select.outputs.compose }} scripts: ${{ steps.select.outputs.scripts }} + k8s: ${{ steps.select.outputs.k8s }} steps: - uses: actions/checkout@v7 with: @@ -497,12 +498,71 @@ 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. + k8s: + name: Kubernetes 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. + - name: Install kubectl v1.36.5 and kubeconform v0.8.0 + env: + KUBECTL_SHA256: 33bc88a24c3b09cf55bfd59e6ca977881b61fc3f25ab65bc80c65b430248648e + run: | + mkdir -p "$HOME/.local/bin" && cd "$RUNNER_TEMP" + curl -fsSLO https://dl.k8s.io/release/v1.36.5/bin/linux/amd64/kubectl + echo "$KUBECTL_SHA256 kubectl" | sha256sum -c - + install -m 0755 kubectl "$HOME/.local/bin/kubectl" + base=https://github.com/yannh/kubeconform/releases/download/v0.8.0 + curl -fsSLO "$base/kubeconform-linux-amd64.tar.gz" + curl -fsSL "$base/CHECKSUMS" | grep ' kubeconform-linux-amd64.tar.gz$' | sha256sum -c - + tar -xzf kubeconform-linux-amd64.tar.gz kubeconform + install -m 0755 kubeconform "$HOME/.local/bin/kubeconform" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Render, validate and assert + env: + DIRS: ${{ needs.changes.outputs.k8s }} + # datreeio/CRDs-catalog pinned to a commit; it carries the Strimzi schemas. + CRDS: https://raw.githubusercontent.com/datreeio/CRDs-catalog/63669a570e231d4f1f8396d229a1de512bcf0a34/{{.Group}}/{{.ResourceKind}}_{{.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") + 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. + 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; } + 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}-: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 + echo "::endgroup::" + done + done < <(jq -r '.[]' <<<"$DIRS") + exit "$failed" + # The one check branch protection requires: passes when every job above # succeeded or was skipped by `changes`, fails otherwise. examples-ok: name: examples-ok if: always() - needs: [changes, jvm, dotnet, go, python, cpp, compose, scripts] + needs: [changes, jvm, dotnet, go, python, cpp, compose, scripts, k8s] runs-on: ubuntu-latest steps: - env: diff --git a/CLAUDE.md b/CLAUDE.md index 887ad5bc..3513287e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,11 @@ examples/ Default infra: **Docker Engine with `docker compose`** on Fedora/RHEL (`lgtm-docker-stack` patterns). Run an example with `docker compose up -d --build --wait` and tear it down with `docker compose down -v`. All examples, including 09, 12, 22 and 24, run on compose. -minikube (`lgtm-minikube-stack` patterns) and OpenShift Local are optional paths added later in +Example 05 also runs on minikube (`examples/05-event-driven/k8s/`, Appendix R): profile `cndp-k8s` +(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. @@ -135,7 +139,7 @@ Do not use `gh cache delete --all`: it also removes the entries the current poli the examples that use it. - Locks are regenerated with `conan lock create --update` (no lockfile in) so transitive packages take the newest ConanCenter version; stale transitive pins are lifted with `override=True` in the - conanfile (fmt, lz4, util-linux-libuuid, jsoncpp). Held on purpose: abseil 20260107.1 (protobuf 6.33.5), + conanfile (fmt, lz4, util-linux-libuuid, jsoncpp). Held back: abseil 20260107.1 (protobuf 6.33.5), protobuf 6.33.5 (grpc is below 7), opentelemetry-proto 1.7.0 (opentelemetry-cpp), taocpp-pegtl 3.2.8 (cppgraphqlgen needs 3.x), lua 5.4.8 (sol2), librdkafka 2.14.2 and opentelemetry-cpp 1.26.0 (ConanCenter behind upstream). flex and bison get `MAKEINFO=true` through a scoped Conan conf, not a fake binary. diff --git a/README.md b/README.md index 4e53dc67..2054ea62 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ the *Designing Cloud-Native APIs* deck series; the decks themselves live in ## What's inside -- **A setup guide, 13 chapters, 17 deep-dive appendices (A–Q), and a glossary** — +- **A setup guide, 13 chapters, 18 deep-dive appendices (A–R), 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, @@ -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–Q, and the glossary | +| `_docs/` | the setup guide, chapters, appendices A–R, 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 | diff --git a/_docs/05-event-driven.md b/_docs/05-event-driven.md index ace22cc6..ce930a50 100644 --- a/_docs/05-event-driven.md +++ b/_docs/05-event-driven.md @@ -1290,8 +1290,9 @@ redelivery is the at-least-once guarantee, and the reason your handler dedupes. `kcat` or the Kafka console tools make the offsets and redelivery visible. 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. +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. --- -*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) +*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) (event publication to order.placed, fan-out to two independent consumer groups (shipping and notification), and idempotent consumers under a redelivered duplicate event).* diff --git a/_docs/29-appendix-p-dotnet-on-linux.md b/_docs/29-appendix-p-dotnet-on-linux.md index dbad799a..184e63d8 100644 --- a/_docs/29-appendix-p-dotnet-on-linux.md +++ b/_docs/29-appendix-p-dotnet-on-linux.md @@ -169,11 +169,9 @@ Every example in this book uses a two-stage Containerfile: FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 \ - dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked \ + dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app @@ -184,10 +182,13 @@ ENTRYPOINT ["dotnet", "OrderService.dll"] ``` **Stage 1 (build)** uses the SDK image (`ubi9/dotnet-100`, .NET 10.0.12, ~780 MB). It -restores NuGet packages first (this layer is cached if the `.csproj` has not changed), -then copies source and publishes. Both steps run as the image's non-root user (UID 1001, -group 0); a BuildKit cache mount keeps the NuGet package cache across builds, and -`--no-restore` makes the publish reuse what the restore step fetched. Every package in the +copies the source and publishes. The publish restores the NuGet packages in the same +`RUN` that mounts the package cache, as the image's non-root user (UID 1001, group 0); the +BuildKit cache mount keeps the package cache across builds, so a rebuild restores from disk +without the network (`sharing=locked` serializes images built in parallel, which would otherwise +extract into the same package directory at once). Restore and publish stay in one step because a separate restore layer +can be reused after BuildKit has dropped the cache mount, and a later publish with +`--no-restore` then fails with NETSDK1064 (packages not found). Every package in the `.csproj` files is pinned to an exact version. Red Hat does not publish a `ubi10` .NET image, so the examples stay on `ubi9` and pin the exact tags. diff --git a/_docs/29-glossary.md b/_docs/29-glossary.md index 165ba853..67409dd2 100644 --- a/_docs/29-glossary.md +++ b/_docs/29-glossary.md @@ -2,7 +2,7 @@ title: "Acronyms" marker: "★" label: "Glossary" -order: 31 +order: 99 part: "Deep-dive appendices" description: "Every acronym used across the book, expanded and placed in context — grouped by where it shows up: the wire, the data layer, events, the platform, security, observability, and the domain." --- diff --git a/_docs/31-appendix-r-minikube.md b/_docs/31-appendix-r-minikube.md new file mode 100644 index 00000000..e730334c --- /dev/null +++ b/_docs/31-appendix-r-minikube.md @@ -0,0 +1,277 @@ +--- +title: "Running Example 05 on minikube" +marker: "R" +label: "Appendix R" +order: 31 +part: "Deep-dive appendices" +description: "Example 05 on a single-node Kubernetes cluster: minikube with the Docker driver, Strimzi for Kafka, kustomize overlays for six languages, loopback NodePorts instead of tunnels, and the same verify.sh against compose and the cluster." +duration: 25 minutes +--- + +Every example in this book runs on `docker compose`. Compose proves the code; it does not prove the +deployment. A cluster adds questions compose never asks: where the configuration and the credential +live, how a service finds Kafka when the broker is a managed resource, what restarts a process that +exits, and how a request from the host reaches a pod. This appendix runs **Example 05** +(event-driven fan-out) on minikube, with the same three services in all six languages and the same +`verify.sh`, and records what changed. + +The scope is one example on one node. It is not a production topology: there is one Kafka broker, +one Postgres instance and no service mesh. High availability, network policy and +autoscaling are outside it. What this appendix +fixes is the contract between an application and its platform: the Service names, the Secret, the +environment variables and the health endpoint. Appendix S reuses that contract on OpenShift Local. + +## The runtime + +minikube runs the Kubernetes control plane and the workload in one container on Docker Engine. +The profile is named `cndp-k8s`: the Docker driver creates a Docker network named after the +profile, and the compose stack already owns a network called `cndp`. + +| Setting | Value | Reason | +|---------|-------|--------| +| Driver | `docker`, context `default` | Same engine as compose; no desktop application | +| Container runtime | `containerd` | The runtime Kubernetes distributions ship | +| Kubernetes | v1.36.5 | Matches the pinned kubectl, so client and server are the same version | +| Resources | 8 GB, 4 CPUs, 40 GB disk | Kafka, Postgres, LGTM and three services | +| Published ports | `127.0.0.1:30080`, `127.0.0.1:30300` | order-service and Grafana, loopback only | +| kubectl context | unchanged after setup | Every command passes `--context cndp-k8s`; `minikube start` runs with `--keep-context`, and `setup-profile.sh` also restores the prior context because minikube's restart path overrides the flag | + +Host access uses NodePorts published when the profile is created. `kubectl port-forward`, +`minikube tunnel` and `minikube service` are not used: forwarded connections drop under load and on +idle, and a drop looks like a broken service. Because the ports are fixed at creation, +`setup-profile.sh` checks with `docker port` that both bind to `127.0.0.1` and not to all interfaces. + +{% include excalidraw.html + file="31-minikube-topology" + alt="The host runs Docker Engine. Inside the node container cndp-k8s there are two namespaces: strimzi holds the Strimzi operator, and cndp-05 holds order-service, the shipping and notification consumers, a Kafka cluster, Postgres, the LGTM stack and the Postgres Secret. The host reaches order-service on 127.0.0.1:30080 and Grafana on 127.0.0.1:30300, and runs kubectl exec against the Kafka and Postgres pods." + caption="Figure R.1 — The node container holds two namespaces; the host reaches two loopback NodePorts and uses kubectl exec for the Kafka and Postgres checks" %} + +## From compose to Kubernetes + +Each compose element has a Kubernetes counterpart, and each counterpart changes something. + +| Compose | Kubernetes | What changes | +|---------|------------|--------------| +| `kafka` (apache/kafka, `kafka:9094`) | Strimzi `Kafka` + `KafkaNodePool`, `cndp-kafka-bootstrap:9092` | An operator owns the broker; the plain listener is in-cluster only | +| `postgres` + init volume | StatefulSet `cndp-postgres`, ConfigMap `postgres-init`, Service `cndp-postgres-rw` | The schema file is the same one compose mounts | +| `apppass` in each service's environment | Secret `cndp-postgres-app`, read through `secretKeyRef` | One place for the demo credential | +| `lgtm` | Deployment `lgtm`, Services `lgtm` (OTLP) and `lgtm-grafana` (NodePort) | Same image, same OTLP endpoint `http://lgtm:4318` | +| `build:` | `docker build` + `minikube image load` | Images are built outside the cluster | +| `depends_on`, `healthcheck` | readiness and startup probes, `rollout status` | The platform retries; ordering is by waiting, not by dependency | +| `ports: 8080:8080` | NodePort 30080 | The host port moves into the NodePort range | +| `docker exec cndp-kafka ...` | `kubectl exec pod/cndp-dual-role-0 -c kafka -- ...` | `verify.sh` maps container names to pods | + +Only the order-service has a `/healthz` endpoint, so it alone has startup and readiness probes +(a 150-second startup window for the JVM and .NET images). The consumers have no HTTP surface; the +cluster restarts them if they exit, and a consumer that cannot reach Kafka retries until the broker +is ready. All twelve consumers start from the earliest offset, so a restart after the topic exists +loses nothing. + +## The manifests + +The manifests live in `examples/05-event-driven/k8s/` and use kustomize, which is built into +`kubectl`: + +``` +k8s/ + namespace.yaml + infra/ Kafka, Postgres, LGTM, the Secret and the schema ConfigMap + base/ three Deployments, the order-service Service + overlays/ + spring-boot/ quarkus/ dotnet/ python/ cpp/ go/ +``` + +`base/` is language-neutral. Its Deployments set the image name to the service name, run with a +restricted security context (non-root, no privilege escalation, all capabilities dropped, +`RuntimeDefault` seccomp, no fixed user ID), read `PG_USER` and `PG_PASSWORD` from the Secret, and +point `OTEL_EXPORTER_OTLP_ENDPOINT` at the LGTM Service. Each overlay does two things: it maps the +service names to `localhost/cndp-05/-:v1`, and it appends the environment +variables that language reads. Kubernetes expands `$(PG_PASSWORD)` in a later variable only if +`PG_PASSWORD` is defined earlier in the list, which is why the credential variables sit in the base. + +{% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %} + +```yaml +# overlays/spring-boot — appended to each Deployment's environment +- name: SPRING_DATASOURCE_URL + value: "jdbc:postgresql://cndp-postgres-rw:5432/appdb" +- name: SPRING_DATASOURCE_USERNAME + value: "$(PG_USER)" +- name: SPRING_DATASOURCE_PASSWORD + value: "$(PG_PASSWORD)" +- name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: "cndp-kafka-bootstrap:9092" +``` + +```yaml +# overlays/quarkus +- name: QUARKUS_DATASOURCE_JDBC_URL + value: "jdbc:postgresql://cndp-postgres-rw:5432/appdb" +- name: QUARKUS_DATASOURCE_USERNAME + value: "$(PG_USER)" +- name: QUARKUS_DATASOURCE_PASSWORD + value: "$(PG_PASSWORD)" +- name: KAFKA_BOOTSTRAP_SERVERS + value: "cndp-kafka-bootstrap:9092" +``` + +```yaml +# overlays/dotnet +- name: DATABASE_URL + value: "Host=cndp-postgres-rw;Port=5432;Database=appdb;Username=$(PG_USER);Password=$(PG_PASSWORD)" +- name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" +- name: SERVICE_VERSION + value: "1.0.0" +``` + +```yaml +# overlays/python — memory limit 256Mi +- name: DATABASE_URL + value: "postgres://$(PG_USER):$(PG_PASSWORD)@cndp-postgres-rw:5432/appdb" +- name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" +``` + +```yaml +# overlays/cpp — memory limit 256Mi +- name: PG_CONNINFO + value: "postgresql://$(PG_USER):$(PG_PASSWORD)@cndp-postgres-rw:5432/appdb" +- name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" +``` + +```yaml +# overlays/go — memory limit 256Mi +- name: DATABASE_URL + value: "postgres://$(PG_USER):$(PG_PASSWORD)@cndp-postgres-rw:5432/appdb" +- name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" +``` + +The variable names are the ones the compose files already use, so the application code is +untouched. The infrastructure directory reads the schema from `../db/init`, which sits outside its +own directory, and kustomize refuses that by default. The scripts render it with the load +restrictor lifted, which keeps one copy of the schema: + +```bash +kubectl kustomize --load-restrictor LoadRestrictionsNone k8s/infra | kubectl --context cndp-k8s apply -f - +``` + +Kafka is a `Kafka` resource, not a Deployment. The Strimzi operator (Helm chart 1.2.0, in its own +namespace and watching all namespaces) turns a single-node KRaft `KafkaNodePool` and a `Kafka` +resource into a broker with a 2 GiB volume that is deleted with the namespace. The plain listener +on port 9092 is the one the applications use, so the Kafka version (4.3.1) and the listener port +are the only Kafka settings the services depend on. + +## Getting the images in + +The cluster does not build images. Each service is built on the host with `docker build`, which +keeps the host's BuildKit caches (a cold C++ build takes half an hour), then copied into the +node's containerd store with `minikube image load`. The Deployments set `imagePullPolicy: Never`, +so a missing image fails fast instead of attempting a pull from a registry that does not have it. +Tags are always `v1`; nothing uses `latest`. + +{% include excalidraw.html + file="31-minikube-image-flow" + alt="A Containerfile feeds docker build, which uses the host BuildKit cache and produces an image named localhost/cndp-05/language-service:v1 in the Docker image store. minikube image load copies it into containerd in the node container under the same name, and the pod runs it with imagePullPolicy Never." + caption="Figure R.2 — Images are built on the host and loaded into the node; pods never pull them" %} + +## Run it + +From `examples/05-event-driven/`, with no other minikube profile or container running: + +```bash +k8s/setup-profile.sh # create the profile; checks the published ports +k8s/build-images.sh go # build and load the three Go images +k8s/deploy.sh go # Strimzi, then Kafka, Postgres, LGTM, then the Go overlay +RUNTIME=kubernetes BASE=http://127.0.0.1:30080 ./verify.sh +``` + +`verify.sh` is the file compose uses. Its container operations go through helper functions, and +`RUNTIME=kubernetes` swaps the implementations: `ctr_exec cndp-kafka ...` becomes +`kubectl exec pod/cndp-dual-role-0 -c kafka -- ...` and `ctr_exec cndp-postgres ...` becomes +`kubectl exec pod/cndp-postgres-0 -- ...`. The ten checks are unchanged. + +A Go run records: + +``` +==> Verifying Example 05: Event-Driven Architecture + + ✓ order-service healthz + ✓ POST /orders returns 201 + ✓ order visible in GET /orders + → waiting for consumers to process events... + ✓ order.placed topic exists in Kafka + ✓ shipment created for order + ✓ notification created for order + ✓ shipping-group consumer group exists + ✓ notification-group consumer group exists + → testing idempotent consumers (re-sending duplicate event)... + ✓ shipping remains idempotent (still 1 row after duplicate) + ✓ notification remains idempotent (still 1 row after duplicate) + +==> Results: 10 passed, 0 failed +==> PASS +``` + +To run all six languages in sequence, with a fresh namespace for each: + +```bash +OUT=/tmp/cndp-k8s-matrix k8s/verify-matrix.sh +``` + +Recorded run on a Fedora 44 host (Docker Engine 29.8.2, minikube v1.39.0, Helm v4.3.0, host build +caches warm). Time covers the image build and load, the deploy, the checks and the namespace teardown. + +| Language | Result | Time | +|----------|--------|------| +| Spring Boot | 10 passed, 0 failed | 361 s | +| Quarkus | 10 passed, 0 failed | 135 s | +| .NET | 10 passed, 0 failed | 134 s | +| Python | 10 passed, 0 failed | 116 s | +| C++ | 10 passed, 0 failed | 103 s | +| Go | 10 passed, 0 failed | 104 s | + +Grafana answers on `http://127.0.0.1:30300` with anonymous access, as in compose. After each +language's `verify.sh`, `verify-matrix.sh` asks Tempo, through the `lgtm` pod, which services it +has received traces from (`/api/search/tag/service.name/values`) and keeps the reply as evidence. +All six languages reported all three services (`order-service`, `shipping-consumer` and +`notification-consumer`). + +## Clean up + +```bash +k8s/teardown.sh --operator # the namespace, the Strimzi release, its CRDs and namespace +minikube stop -p cndp-k8s # free the memory; the profile and its images stay +``` + +`teardown.sh` never deletes the profile. `minikube delete -p cndp-k8s` removes it, and the next +`setup-profile.sh` recreates it. Stop the profile before you start a compose stack or another +minikube profile: the scripts refuse to start while anything else is running. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `setup-profile.sh` refuses to start | Another minikube profile or a container is running | Stop it yourself; the script never stops anything | +| Host port 30080 or 30300 is in use | Another process holds it | Free it, then run the script again; ports are fixed at creation | +| `docker port` shows `0.0.0.0` | The profile was created without a loopback address | `minikube delete -p cndp-k8s`, then run `setup-profile.sh` again | +| Pod `ErrImageNeverPull` | The image was not loaded into this profile | `k8s/build-images.sh ` | +| Kafka never becomes Ready | Operator not running or the node is out of memory | `kubectl --context cndp-k8s -n strimzi get pods`; check node memory | +| `kubectl` talks to the wrong cluster | Commands without `--context` | Pass `--context cndp-k8s`; the scripts do | +| order-service restarts during startup | The startup probe window is shorter than the JVM start on a loaded host | Raise `failureThreshold` on the startup probe in `base/` | + +### Cross-check it yourself + +Place an order through `127.0.0.1:30080`, then run +`kubectl --context cndp-k8s -n cndp-05 exec pod/cndp-postgres-0 -- psql -U appuser -d appdb -c "SELECT * FROM shipments"` +and confirm one row for the order. Scale the shipping consumer to zero replicas with +`kubectl --context cndp-k8s -n cndp-05 scale deployment/shipping-consumer --replicas=0`, place another order +and query again: the count is 0. Scale it back to one replica and the count becomes 1, because the +consumer group resumes from its last committed offset. + +The manifests and scripts are in [`examples/05-event-driven/k8s/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/k8s/). + +--- +*Verification status: verified — [`examples/05-event-driven/k8s/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/k8s/) passes 10/10 checks in each of the six languages on minikube v1.39.0 (Docker driver, containerd, Kubernetes v1.36.5, Strimzi 1.2.0, 2026-10-11), using the same `verify.sh` as compose with `RUNTIME=kubernetes`. The consumer scale-down and scale-up in the cross-check was run by hand and is not part of the script.* diff --git a/_example_pages/prerequisites.md b/_example_pages/prerequisites.md index 8f3d3f48..80ebcdf0 100644 --- a/_example_pages/prerequisites.md +++ b/_example_pages/prerequisites.md @@ -77,21 +77,33 @@ docker run --rm hello-world ## Kubernetes tools (optional) -The examples run on `docker compose` and need no cluster. A later part of the tutorial adds -optional minikube and OpenShift Local paths; install these tools only when you reach them. +The examples run on `docker compose` and need no cluster. Appendix R runs Example 05 on +minikube; install these tools when you reach it. minikube uses the Docker driver on the same +Docker Engine as compose, with the `default` Docker context. ### minikube +Pinned to v1.39.0, with the binary checked against its published SHA-256 file: + ```bash -curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 -sudo install minikube-linux-amd64 /usr/local/bin/minikube +MINIKUBE_VERSION=v1.39.0 +mkdir -p ~/.local/bin && cd "$(mktemp -d)" +curl -fsSLO "https://github.com/kubernetes/minikube/releases/download/${MINIKUBE_VERSION}/minikube-linux-amd64" +echo "$(curl -fsSL "https://github.com/kubernetes/minikube/releases/download/${MINIKUBE_VERSION}/minikube-linux-amd64.sha256") minikube-linux-amd64" | sha256sum -c - +install minikube-linux-amd64 ~/.local/bin/minikube ``` ### kubectl +Pinned to v1.36.5 (the Kubernetes version the minikube profile runs), checked against its +published SHA-256: + ```bash -curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" -sudo install kubectl /usr/local/bin/kubectl +KUBECTL_VERSION=v1.36.5 +mkdir -p ~/.local/bin && cd "$(mktemp -d)" +curl -fsSLO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" +echo "$(curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256") kubectl" | sha256sum -c - +install kubectl ~/.local/bin/kubectl ``` ### Helm @@ -239,7 +251,7 @@ container. | Stack | Memory | Disk | CPU | |-------|--------|------|-----| | Docker compose examples | ~2-4 GB | ~3 GB for images | 2+ cores | -| Minikube (optional) | ~6-8 GB | ~10 GB | 4+ cores | +| Minikube, Example 05 (optional) | 8 GB for the node | 40 GB (profile disk) | 4+ cores | The LGTM observability stack (Grafana, Loki, Tempo, Mimir) is the heaviest shared component at ~1.5 GB. diff --git a/_plans/modernization-plan.md b/_plans/modernization-plan.md index 403cb9df..fa1b754a 100644 --- a/_plans/modernization-plan.md +++ b/_plans/modernization-plan.md @@ -30,11 +30,11 @@ Each step gets its own branch, a PR, green CI and a squash-merge before the next | S5 | chore/python-3.14 | 5 | done (19/19 Python + 28 PASS after the Opus review) | | S6 | chore/go-toolchain | 2+4 | done (18/18 Go + 06 in all six languages PASS after the Opus review; closes #34) | | S7 | chore/cpp-toolchain-ubi10 | 2+4 | done (111/111 PASS after two Opus repair rounds; closes #35) | -| S8 | chore/infra-images | 2 | validated (awaiting PR/CI) | -| S9 | docs/minikube-docker-driver (+ 05 on minikube) | 7 | pending | +| S8 | chore/infra-images | 2 | done (111/111 PASS; two Opus repair rounds; PR #66; lgtm-skills #45 + r5.x; main run green, Actions cache 2.4 GB) | +| S9 | docs/minikube-docker-driver (+ 05 on minikube) | 7 | executed (awaiting validation) | | S10 | feature/openshift-local-appendix | 8 | pending | | S11 | docs/professional-voice | 9 | pending | -| S12 | docs/deck-refresh | 10 | pending | +| S12 | docs/deck-refresh | 10 (expanded 2026-10-11) | pending | ## Target pins (verified 2026-10-09; re-check at the start of each branch) @@ -57,8 +57,11 @@ Each step gets its own branch, a PR, green CI and a squash-merge before the next | Envoy | envoyproxy/envoy:v1.39.3 | | kafka-ui | removed (use kcat) | | Test tools | newman 6.2.3, htmlextra 1.23.1, hey v0.1.5, ghz v0.121.0, conftest 0.71.1 | -| minikube | v1.39.0, Kubernetes v1.36.5 | -| Operators | Strimzi 1.2.0, CNPG v1.30.1, KEDA v2.21.0, Istio 1.31.1 | +| minikube | v1.39.0, Kubernetes v1.36.5, profile `cndp-k8s` (NodePorts 30080 and 30300 on 127.0.0.1) | +| kubectl | v1.36.5 (kustomize v5.8.1) | +| Helm | v4.3.0 | +| kubeconform | v0.8.0, CRDs-catalog commit 63669a570e231d4f1f8396d229a1de512bcf0a34 | +| Operators | Strimzi 1.2.0 (chart `oci://quay.io/strimzi-helm/strimzi-kafka-operator` 1.2.0; Kafka image `quay.io/strimzi/kafka:1.2.0-kafka-4.3.1`), CNPG v1.30.1, KEDA v2.21.0, Istio 1.31.1 | | CRC | crc 2.64.0 / OCP 4.22.14; AMQ Streams per lgtm-crc (check packagemanifest) | ## Key facts from the inventory @@ -80,9 +83,14 @@ Each step gets its own branch, a PR, green CI and a squash-merge before the next - S2–S7: every project builds; verify.sh green for that language's variants; images pinned, with no :latest. - S8: 0 :latest, -latest or kafka-ui; full matrix green. - S9: minikube appendix with evidence; profile stopped. -- S10: Appendix R with evidence for 5 languages; scrub clean; CRC stopped and clean. +- S10: Appendix S with evidence for 5 languages; scrub clean; CRC stopped and clean. - S11: voice scan reports zero ban-tier hits and is enforced in CI. -- S12: 0 references to the old container engine in the decks; current versions; OpenShift Local slides; rendered and inspected. +- S12 (scope expanded by the user 2026-10-11): review every presentation in `lgtm-presentation/` (the six Designing-Cloud-Native-APIs decks, K8s-Environment-Access-Policies, Network-Policy-Process-Advisory, and their .md/.docx companions) for: + - version changes (every version on a slide matches the S2–S8 pins); + - stack changes (Docker Engine, not the old container engine; locked libraries such as Streamiz, faust-streaming, goka, librdkafka C++, C++23; kafka-ui removed); + - code changes (every code snippet on a slide matches the current example code and APIs, e.g. Spring Boot 4 / Jackson 3, Quarkus 3.40, .NET 10, Python 3.14, Go 1.27, C++23); + - wording and professionalism (lgtm-professional-voice: zero ban-tier hits); + - 0 references to the old container engine; minikube and OpenShift Local slides; every deck rendered and inspected. ## Evidence @@ -161,6 +169,56 @@ Defects the baseline found and fixed in S1: - Chapter .NET codetabs (05, 06, 18, 22) compile against the pinned packages. - Live: **19/19 .NET targets PASS** on the final tree after the Opus review. +### S9 minikube (2026-10-11) + +Fedora 44, Docker Engine 29.8.2, minikube v1.39.0, kubectl v1.36.5 (kustomize v5.8.1), **Helm v4.3.0**, profile `cndp-k8s` (docker driver, containerd 2.3.4, Kubernetes v1.36.5, 8 GB, 4 CPUs, 40 GB), NodePorts 30080 and 30300 published on 127.0.0.1 only (`docker port cndp-k8s`). Evidence: `/tmp/claude-1000/-home-rsedor-Dev/0dea024d-0bf0-4ae5-bddd-7dec0cf60b22/scratchpad/s9/` (`00-prechecks.txt`, `01-setup-profile.log`, `matrix/` with `results.tsv`, per-language logs and evidence, `docker-path/`, `10-static-checks.txt`, `11-final-state.txt`). + +Pins re-verified at the start: Strimzi chart and operator 1.2.0 (Kafka image `quay.io/strimzi/kafka:1.2.0-kafka-4.3.1`, Kafka 4.3.1, Kafka CR `kafka.strimzi.io/v1`), postgres 18.6-alpine, otel-lgtm 0.36.0, kubeconform v0.8.0, actionlint 1.7.12, shellcheck 0.11.0, CRDs-catalog commit 63669a5. Kubernetes 1.37.1 exists upstream and minikube v1.39.0 lists 1.37.0 as its newest; v1.36.5 stays because the host kubectl and the other profiles are 1.36.5 and the plan was approved with it. + +**Live run: 6/6 languages `==> Results: 10 passed, 0 failed`** with `RUNTIME=kubernetes` (`k8s/verify-matrix.sh`, fresh namespace per language, images built with docker and loaded with `minikube image load`, all pods 0 restarts, Grafana `/api/health` `database: ok` on 127.0.0.1:30300): + +| Language | Result | Time | +|---|---|---| +| spring-boot | 10 passed, 0 failed | 361 s | +| quarkus | 10 passed, 0 failed | 135 s | +| dotnet | 10 passed, 0 failed | 134 s | +| python | 10 passed, 0 failed | 116 s | +| cpp | 10 passed, 0 failed | 103 s | +| go | 10 passed, 0 failed | 104 s | + +An earlier single-language Spring Boot run (205 s) also passed first time. Docker-path regression: `scripts/verify-matrix.sh 05-event-driven/go` PASS 10/10 (57 s). A hand cross-check (scale shipping-consumer to 0, place an order, 0 shipments; scale to 1, 1 shipment) passed. + +End state: `teardown.sh --operator` removed the `cndp-05` namespace, the Strimzi release, all Strimzi CRDs and the `strimzi` namespace; `minikube stop -p cndp-k8s` left the profile Stopped (not deleted); every other profile unchanged (helm4dev still broken, left alone); kubectl current-context unset before and after; no containers running. + +Repair round 1 (validator findings): + +- Current-context: the earlier "never changed" claim was wrong for restarts. When a stopped docker-driver profile restarts and its API-server host port changes, minikube v1.39.0 sets current-context to the profile even with `--keep-context` (reproduced: a raw `minikube start -p cndp-k8s --keep-context` after a stop left `cndp-k8s` as current-context). `setup-profile.sh` now records the context before any start, restores it (unset or `use-context`) and fails if it differs. Tested on a copy of the kubeconfig: restart from unset, restart from a throwaway context, already-running no-op; then on the real kubeconfig, which stayed unset. The create path was not re-run (it needs a new profile name or a delete, both out of bounds). +- `setup-profile.sh` also runs the host-port check whenever the profile is not running and compares Driver, ContainerRuntime and KubernetesVersion with docker, containerd and v1.36.5 (tested: `KUBERNETES_VERSION=v1.35.0` fails with a clear message). +- `teardown.sh --operator` is idempotent (`grep` guarded under `pipefail`, `--ignore-not-found` on the CRD delete); two runs in a row both exit 0. +- Quarkus OTLP: Quarkus defaults to gRPC but 4318 is OTLP/HTTP, so exports failed with "First received frame was not SETTINGS" and Tempo listed no services. `quarkus.otel.exporter.otlp.protocol=http/protobuf` added to the ten `application.properties` of Examples 02, 03, 05 and 06 that export to 4318 without it (01, 04 and 18 already had it; 11 and 17 use 4317). Compose regression, one at a time: 02/quarkus 10/10, 03/quarkus 8/8, 05/quarkus 10/10, 06/quarkus 11/11, all PASS. +- Traces per language on k8s, from Tempo through the lgtm pod after each `verify.sh` (a non-counted step in `k8s/verify-matrix.sh`; `verify.sh` is still 10 checks): Spring Boot, Quarkus and .NET report order-service, shipping-consumer and notification-consumer; Python reported order, shipping and notification (renamed in round 2). The .NET exporter defaulted to gRPC against 4318 and the consumers never registered their `ActivitySource`; both fixed (HTTP protobuf with `/v1/traces`, `AddSource`), compose 05/dotnet 10/10. Go and C++ reported no services in round 1 because Example 05 carried no tracing code in those languages; round 2 below adds it. +- All six languages 10/10 on k8s in the repair run (spring-boot 185 s, quarkus 382 s, python 145 s, cpp 131 s, go 133 s; dotnet re-run after the fix 173 s). A stale BuildKit cache mount made one .NET build fail with NETSDK1064; the `--no-cache` workaround was replaced by a Containerfile fix in round 2. +- `ci_changed_projects.py`: a deleted or missing file under `k8s/` still selects the directory while it exists (tested with `--files`: existing manifest, deleted `base/x.yaml`, `.sh`, README, `db/init`). +- shellcheck `source-path=SCRIPTDIR` added; clean on the k8s scripts. `verify-matrix.sh` no longer prints the language twice. `verify.sh`: a failed duplicate-event send no longer aborts and cannot pass the two idempotency checks vacuously. The .NET overlay sets `SERVICE_VERSION` on the order-service only, as compose does. +- End state: `teardown.sh --operator` twice (0, 0), `minikube stop -p cndp-k8s`, current-context unset, no containers running. + +Repair round 2: + +- Go and C++ tracing in Example 05, all six services. Go: OTel Go SDK 1.47.0 (the newest stable, same as Example 11), `otlptracehttp` to `OTEL_EXPORTER_OTLP_ENDPOINT`, `TraceContext` global propagator, `otelhttp` server span for the HTTP requests, a producer span around the produce with the context injected as a Kafka header, and one consumer span per record extracted from that header; `log/slog` stays. Indirect modules (grpc 1.84.0, x/net 0.61.0, genproto and others) match Example 11. C++: opentelemetry-cpp 1.26.0 (the ConanCenter newest and the Example 11 version) with the Example 11 `telemetry.hpp` (OTLP/HTTP, traces only) plus `traceparent` parsing; order-service has a server span for `POST /orders`, a child producer span and the `traceparent` header on the Kafka message, spdlog 1.17.0 for a trace-correlated log line, and each consumer has one span per record parented on the header. `nlohmann_json` 3.12.0 and `fmt` 12.2.0 are `override=True` as in 11. The three `conan.lock` files were regenerated with `conan lock create --update` inside the builder image (Conan 2.33.0, GCC 15, C++23, Release) against the `cndp-conan2` cache. +- Service names are `order-service`, `shipping-consumer` and `notification-consumer` in all six languages. Python took them from hard-coded `order`, `shipping` and `notification`; it now reads `OTEL_SERVICE_NAME` (set in compose and k8s) with those names as the default. Nothing else depended on the old names (`k8s/verify-matrix.sh` counts services, not names). `OTEL_SERVICE_NAME` was added to the Go and Python compose files. +- NETSDK1064 root cause: the separate `dotnet restore` RUN stayed cached as a layer while the BuildKit cache mount that held the packages was gone, so a later `publish --no-restore` found no packages. All 32 .NET Containerfiles (every `examples/*/dotnet` plus 30) now restore and publish in one `RUN` that mounts the cache; the csproj-only COPY and `--no-restore` are gone. The mount is `sharing=locked` because compose builds two images at once and two restores extracting into one package directory raced (`Could not find file '/nuget-cache/grpc.tools/...'` on 02, found by the first matrix run). Proof on a throwaway `docker buildx create` builder (removed afterwards): the old Containerfile.order built once, cache mounts cleared, source touched, second build failed with NETSDK1064 (Confluent.Kafka 2.16.0 not found); the new one passed the same sequence, and a warm rebuild restored in 0.8 s (14 s total). `DOCKER_BUILD_OPTS` is removed from `build-images.sh` and Appendix P describes the new pattern; the `29-container-layers` diagram text was regenerated. +- Compose, one at a time: `05-event-driven` go, cpp and python 10/10; `scripts/verify-matrix.sh dotnet` 19/19 PASS (01 to 06, 11, 12, 16 to 19, 21, 22, 24 to 27, 30). +- k8s: `k8s/verify-matrix.sh` 10/10 in all six languages (spring-boot 184 s, quarkus 385 s, dotnet 142 s, python 120 s, cpp 111 s, go 107 s). Tempo reported `order-service`, `shipping-consumer` and `notification-consumer` for every language. Appendix R now says so. +- End state: `teardown.sh --operator` exit 0, `minikube stop -p cndp-k8s`, no containers running, real current-context unset (all kubectl and minikube calls used a copy of the kubeconfig). + +Static checks: `validate_site.py` 0 errors, Jekyll build OK (ruby:3.3 container, because the host has no ruby-devel for native gems), actionlint 1.7.12 clean, shellcheck clean on the k8s scripts and `verify.sh`, kubeconform v0.8.0 `-strict` against 1.36.5: infra 9/9 valid, six overlays 4/4 valid each, 0 invalid; selector cases: `k8s/base/*.yaml` selects only `k8s`, `k8s/*.sh` selects only `scripts`, `db/init/*.sql` selects `k8s`, `k8s/README.md` selects nothing, `_infra` selects everything. + +Deviations from the plan: added `k8s/lib.sh` (shared settings for the five scripts); two plan lines naming the old container engine were reworded because `validate_site.py` scans `_plans/`; `kraftMetadata` and `metadataVersion` are left to the Strimzi defaults (4.3-IV0). + +Final Opus recheck (2026-10-11): go. Go and C++ traces verified end to end in Tempo (one trace: `POST /orders` server span, `order.placed publish` producer span, both consumer spans). Follow-ups applied before the PR: +- `setup-profile.sh` restores the kubectl context from an EXIT trap (covers a failed `minikube start` and Ctrl-C) and uses `kubectl config set current-context`, so a prior context whose entry is missing is restored too. Stub test: a start that switches the context and exits 1 leaves it unset (or `ghost ctx`) as before. +- Python 05 now injects and extracts trace context in the Kafka headers. Compose: 05/python 10/10, and Tempo shows one `POST /orders` trace with the producer span and both consumer spans. + ### S7 C++23 (2026-10-10) - 31 C++ projects on C++23 (`CMAKE_CXX_STANDARD 23`, required, no extensions), CMake policy 3.31, `-Wall -Wextra` clean on GCC 15.2. Builder `ubi10/ubi:10.2-1791444044` with `gcc-toolset-15`; CMake 4.4.4 and Ninja 1.13.2 from the upstream release binaries (sha256-verified; UBI ships cmake 3.31 and ninja 1.11); Conan 2.33.0 in a pinned venv. The Conan home is a BuildKit cache mount, packages are deployed next to the sources (`--deployer=full_deploy`) so later layers and the GHA layer cache do not need the mount. Runtime `ubi10/ubi-minimal:10.2-1791444377`, UID 1001 group 0, libstdc++/libgcc linked statically (gcc-toolset-15's libstdc++ is newer than the one in ubi-minimal; proven by running the binaries on ubi-minimal). diff --git a/_plans/s9-minikube-plan.md b/_plans/s9-minikube-plan.md new file mode 100644 index 00000000..557033c6 --- /dev/null +++ b/_plans/s9-minikube-plan.md @@ -0,0 +1,65 @@ +# S9 plan — Example 05 on minikube, Appendix R + +Approved by the user 2026-10-11. Opus plan; Sonnet executes on `docs/minikube-docker-driver`; Opus validates. + +## Findings that shape the design + +1. Profile is **`cndp-k8s`**, not `cndp`: the docker driver creates a Docker network named after the profile, and `_infra/compose-lgtm.yaml` already names the compose network `cndp`. +2. `minikube start` switches kubectl's current-context unless `--keep-context` is passed. Always pass it; show current-context unchanged before and after. +3. Existing profiles (capstone, datamesh, eip, helm4dev (broken), istio, mea, minikube, mof-capstone) are never started, stopped or modified. +4. Ordering comes from the `order` front matter. minikube is **Appendix R** (`_docs/31-appendix-r-minikube.md`, `order: 31`, `marker: "R"`). S10 OpenShift Local becomes **Appendix S** (order 32). Glossary goes to `order: 99` (keep its filename). +5. S10 reuses the environment contract (Service, Secret, ConfigMap names), the Kafka CR shape and `verify.sh RUNTIME=kubernetes` with `KUBECTL=oc`; not the kustomize files as-is. +6. kustomize cannot read `../db/init/*.sql` under the default load restrictor: render with `kubectl kustomize --load-restrictor LoadRestrictionsNone … | kubectl apply -f -` (scripts and CI). Single source for the schema. +7. Files under `examples/05-event-driven/k8s/` would currently trigger the 05 fallback (all 18 projects). Add a `k8s` kind to the selector. This PR edits `EVERYTHING_TRIGGERS` files, so its own CI runs the full matrix once (expected). +8. All 18 consumers use `earliest`; only order-service has `/healthz`; 05 `verify.sh` only uses `ctr_exec` against Kafka and Postgres. + +## Approach + +- Cluster: `minikube start -p cndp-k8s --driver=docker --container-runtime=containerd --kubernetes-version=v1.36.5 --memory=8g --cpus=4 --disk-size=40g --ports=127.0.0.1:30080:30080,127.0.0.1:30300:30300 --keep-context`. +- Kafka: Strimzi 1.2.0 operator (Helm OCI chart `oci://quay.io/strimzi-helm/strimzi-kafka-operator`, namespace `strimzi`, `watchAnyNamespace=true`) + `KafkaNodePool` `dual-role` + `Kafka` `cndp` (version 4.3.1, KRaft, plain 9092 tls false, auto-create topics, replication 1, no entity operator). +- Postgres: plain StatefulSet `cndp-postgres` on `docker.io/library/postgres:18.6-alpine`, schema ConfigMap at `/docker-entrypoint-initdb.d`, volume at `/var/lib/postgresql`, Service `cndp-postgres-rw:5432`. +- Observability: one `docker.io/grafana/otel-lgtm:0.36.0` Deployment; Service `lgtm` (4318), Service `lgtm-grafana` NodePort 3000→30300. +- Apps: kustomize `infra/`, `base/` (3 Deployments; order-service NodePort 8080→30080), `overlays/{spring-boot,quarkus,dotnet,python,cpp,go}`. +- Images: `docker build` with each Containerfile (keeps host BuildKit caches) → `minikube -p cndp-k8s image load --overwrite=true`, `imagePullPolicy: Never`, names `localhost/cndp-05/-:v1`. +- Verify: existing `verify.sh` with `RUNTIME=kubernetes`, `BASE=http://127.0.0.1:30080`. Fresh namespace `cndp-05` per language. +- Rejected: `minikube image build` (loses caches), registry addon, CNPG (peer auth breaks psql check; S10 doesn't use it), plain Kafka StatefulSet, full LGTM Helm stack, no observability, one language only, Helm chart for S9, port-forward/tunnel/`minikube service` (forbidden). + +## Steps + +0. Plan housekeeping: S9 row "in progress"; S10 acceptance "Appendix S"; pins table adds kubectl 1.36.5, kubeconform v0.8.0, Strimzi chart 1.2.0, CRDs-catalog SHA, profile `cndp-k8s` ports 30080/30300. +1. Re-verify pins (gh release list, skopeo list-tags, `helm show chart/values` for Strimzi 1.2.0; confirm Kafka 4.3.1 and Kubernetes 1.36 supported). +2. Manifests in `examples/05-event-driven/k8s/`: + - `infra/` (namespace `cndp-05`): secretGenerator `cndp-postgres-app` (appuser/apppass, same demo creds as compose, `disableNameSuffixHash: true`); configMapGenerator `postgres-init` from `../../db/init/*.sql`; `kafka.yaml` (nodepool persistent-claim 2Gi `deleteClaim: true`, requests 250m/1Gi, limit 2Gi); `postgres.yaml` (same env/flags as compose, readiness `pg_isready -U appuser -d appdb`); `lgtm.yaml` (GF_AUTH_* as compose, readiness `/api/health` on 3000, ~1.5Gi limit). + - `base/`: label `app.kubernetes.io/part-of: cndp-05`; Deployments order-service, shipping-consumer, notification-consumer; restricted-v2-ready securityContext (runAsNonRoot, no privilege escalation, drop ALL, seccomp RuntimeDefault, no fixed UID); `PG_USER`/`PG_PASSWORD` from secretKeyRef; `OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318`; order-service startup+readiness `/healthz:8080` (startup ~150 s); requests set; memory limits ~768Mi JVM/.NET, ~256Mi Go/C++/Python. + - `overlays//`: `images:` mapping; JSON6902 `add /spec/template/spec/containers/0/env/-` env patches with the compose variable names so `$(PG_PASSWORD)` expands. Smoke-render every overlay and check env order. +3. Scripts (all explicit `-p`/`--context`/`--kube-context`; never `minikube config set`; never change current-context): `setup-profile.sh` (preflight Docker context default, minikube ≥ v1.39.0, inotify; refuse if another profile Running; `ss -ltn` port check; create/start; assert `docker port cndp-k8s` binds 127.0.0.1), `build-images.sh …` (18-row table; .NET uses `dotnet/` context with `-f Containerfile.`), `deploy.sh ` (helm upgrade --install Strimzi; apply ns; render+apply infra; wait Kafka Ready 600 s, postgres, lgtm; render+apply overlay; rollout status; poll /healthz), `teardown.sh [--operator]` (never deletes the profile; prints stop/delete commands), `verify-matrix.sh [langs]` (build, deploy, verify, evidence, namespace teardown; TSV + logs to `$OUT`, default outside the repo), `README.md`. +4. `examples/05-event-driven/verify.sh`: `kubernetes)` runtime with `KUBECTL`, `KUBE_CONTEXT` (default `cndp-k8s`), `KUBE_NAMESPACE` (default `cndp-05`); `ctr_target` maps `cndp-kafka`→`pod/cndp-dual-role-0 -c kafka`, `cndp-postgres`→`pod/cndp-postgres-0` (overridable `KAFKA_TARGET`, `POSTGRES_TARGET`); other `ctr_*` helpers error "not supported on kubernetes". Docker path unchanged; still 10 checks; shellcheck clean. +5. CI: `scripts/ci_changed_projects.py` new `k8s` kind (files under `examples//k8s/` select that dir before the language fallback; `examples//db/` changes also select `/k8s`; `k8s/*.sh` into `scripts`; `select_everything` includes them). `.github/workflows/examples.yml`: `changes` output `k8s`; job `k8s` installs kubectl v1.36.5 (sha256) and kubeconform v0.8.0 (CHECKSUMS), renders infra + every overlay into `kubeconform -strict -summary -kubernetes-version 1.36.5` with the CRDs-catalog schema location pinned to commit `63669a570e231d4f1f8396d229a1de512bcf0a34`, plus grep assertions (no `:latest`, app images `localhost/cndp-05/-`, `imagePullPolicy: Never`, nodePorts 30080/30300); add `k8s` to `examples-ok.needs`. actionlint clean. +6. Diagrams: `scripts/gen_31_diagrams.py` (pattern of `gen_30`) → `assets/diagrams/31-minikube-topology.{svg,excalidraw}` and `31-minikube-image-flow.{svg,excalidraw}`; QA-render to PNG. +7. Appendix `_docs/31-appendix-r-minikube.md` ("Running Example 05 on minikube", marker R, order 31, part "Deep-dive appendices"): why a cluster changes things; the runtime; Figure R.1; compose→Kubernetes mapping table; kustomize layout with a six-tab codetabs block of overlay env patches (Spring Boot, Quarkus, .NET, Python, C++, Go); Figure R.2; run it; recorded verify output; clean up; troubleshooting table; cross-check; verification footer. No mention of the old container engine, no positional terms, `{% raw %}` around any `{{`. Voice scan ban tier must be 0. +8. Site/docs: glossary `order: 99`; `index.html` + `README.md` "eighteen … A–R"; `_docs/05-event-driven.md` cross-check sentence + footer; `examples/05-event-driven/README.md` "Run on minikube"; both prerequisites files pin minikube v1.39.0 and kubectl v1.36.5 with sha256 checks and a docker-driver note and resource row; CLAUDE.md minikube line. `validate_site.py` 0 errors; Jekyll build. +9. Live run: no other profile running, no compose containers; record current-context; `setup-profile.sh`; `verify-matrix.sh` all six; each `10 passed, 0 failed`; Grafana `/api/health` once; `teardown.sh --operator`; `minikube stop -p cndp-k8s`; current-context unchanged; evidence section in `_plans/modernization-plan.md`. +10. Opus validation (re-render, review, live rerun of at least a JVM language and C++), repairs, PR, CI (full matrix once), squash-merge. + +## Acceptance criteria + +- Renders: infra + six overlays render; kubeconform v0.8.0 `-strict` 0 invalid against 1.36.5. +- No `:latest`; pulled images fully qualified; app images `localhost/cndp-05/-:v1`. +- `cndp-k8s` docker/containerd/v1.36.5; no other profile changed. +- `docker port cndp-k8s` only `127.0.0.1:` for 30080 and 30300; no port-forward/tunnel/`minikube service` anywhere. +- `RUNTIME=kubernetes verify.sh` 10/10 for all six languages; docker-path verify still passes for at least one language. +- current-context unchanged; run ends with `cndp-k8s` Stopped (not deleted), namespaces `cndp-05` and `strimzi` removed first. +- Appendix R with order 31; glossary order 99; R.1 and R.2 as svg + excalidraw. +- `validate_site.py` 0 errors; Jekyll builds; voice scan 0 ban-tier hits on new/changed docs. +- Prerequisites pin minikube v1.39.0, kubectl v1.36.5 with sha256; CLAUDE.md updated; A–R in index and README. +- Plan file: S9 evidence; S10 says Appendix S; pins table updated. +- Selector: a `k8s/base/x.yaml` change selects only `k8s`; a `k8s/*.sh` change selects `scripts`; actionlint clean. +- PR checks green incl. new `k8s` job; squash-merged. + +## Pins (re-verify at execution start) + +minikube v1.39.0; Kubernetes v1.36.5; kubectl v1.36.5 (kustomize v5.8.1); Strimzi operator/chart 1.2.0 (Kafka image expected `quay.io/strimzi/kafka:1.2.0-kafka-4.3.1`); Kafka 4.3.1; postgres 18.6-alpine; otel-lgtm 0.36.0; kubeconform v0.8.0; CRDs-catalog `63669a570e231d4f1f8396d229a1de512bcf0a34`. Helm v4.3.0 on the host (upgraded 2026-10-11 at the user's request to match the prerequisites pin; checksum-verified install in `~/.local/bin/helm`). Scripts may assume Helm 4. + +## Risks + +Network collision (profile name); current-context switch (`--keep-context`); load restrictor (flag, or `kubectl create configmap --dry-run` fallback); `$(PG_PASSWORD)` env order; `localhost/` image names in containerd (fallback bare names, record it); Kafka CLI memory in broker pod (2Gi); JVM/.NET OOM; consumer CrashLoopBackOff (two-phase apply); Strimzi version support; CRDs-catalog schema drift (`-skip Kafka,KafkaNodePool` with a comment if needed); full CI matrix once; port collisions; other running stacks (refuse, never auto-stop); root images fine on minikube (S10 swaps); validator word rules; node disk (40g). diff --git a/assets/diagrams/29-container-layers.excalidraw b/assets/diagrams/29-container-layers.excalidraw index 45e8ccdc..0f45d7c4 100644 --- a/assets/diagrams/29-container-layers.excalidraw +++ b/assets/diagrams/29-container-layers.excalidraw @@ -4,7 +4,7 @@ "source": "https://excalidraw.com", "elements": [ { - "id": "r1169652895", + "id": "r628116478", "type": "rectangle", "x": 10, "y": 10, @@ -23,8 +23,8 @@ "roundness": { "type": 3 }, - "seed": 1145353725, - "versionNonce": 853854461, + "seed": 889772108, + "versionNonce": 623412524, "isDeleted": false, "boundElements": [], "updated": 1, @@ -32,7 +32,7 @@ "locked": false }, { - "id": "t1313067066", + "id": "t1844254648", "type": "text", "x": 22, "y": 16, @@ -49,8 +49,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1197526914, - "versionNonce": 481798732, + "seed": 490072115, + "versionNonce": 1366933361, "isDeleted": false, "boundElements": [], "updated": 1, @@ -67,7 +67,7 @@ "baseline": 12 }, { - "id": "r1834330621", + "id": "r1710721307", "type": "rectangle", "x": 430, "y": 10, @@ -86,8 +86,8 @@ "roundness": { "type": 3 }, - "seed": 1137185759, - "versionNonce": 1818508283, + "seed": 1849290047, + "versionNonce": 1109975311, "isDeleted": false, "boundElements": [], "updated": 1, @@ -95,7 +95,7 @@ "locked": false }, { - "id": "t1215563348", + "id": "t1790639410", "type": "text", "x": 442, "y": 16, @@ -112,8 +112,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1329966767, - "versionNonce": 1182827607, + "seed": 1827930084, + "versionNonce": 1659514163, "isDeleted": false, "boundElements": [], "updated": 1, @@ -130,7 +130,7 @@ "baseline": 12 }, { - "id": "r236607844", + "id": "r1817366739", "type": "rectangle", "x": 30, "y": 55, @@ -149,8 +149,8 @@ "roundness": { "type": 3 }, - "seed": 1233388649, - "versionNonce": 290073684, + "seed": 594336189, + "versionNonce": 1116091386, "isDeleted": false, "boundElements": [], "updated": 1, @@ -158,7 +158,7 @@ "locked": false }, { - "id": "t616657006", + "id": "t1281452352", "type": "text", "x": 40, "y": 63, @@ -175,8 +175,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 363176981, - "versionNonce": 1335770279, + "seed": 1772498500, + "versionNonce": 1735123647, "isDeleted": false, "boundElements": [], "updated": 1, @@ -193,7 +193,7 @@ "baseline": 14 }, { - "id": "t625313620", + "id": "t1273005124", "type": "text", "x": 40, "y": 83, @@ -210,8 +210,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1884878255, - "versionNonce": 1357304673, + "seed": 996547272, + "versionNonce": 1453481030, "isDeleted": false, "boundElements": [], "updated": 1, @@ -228,7 +228,7 @@ "baseline": 11 }, { - "id": "r1371069686", + "id": "r480267144", "type": "rectangle", "x": 230, "y": 55, @@ -247,8 +247,8 @@ "roundness": { "type": 3 }, - "seed": 1529513020, - "versionNonce": 121906605, + "seed": 1210446418, + "versionNonce": 284115391, "isDeleted": false, "boundElements": [], "updated": 1, @@ -256,7 +256,7 @@ "locked": false }, { - "id": "t591612257", + "id": "t81605889", "type": "text", "x": 240, "y": 63, @@ -273,8 +273,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1586864327, - "versionNonce": 487601204, + "seed": 1538418253, + "versionNonce": 1896834139, "isDeleted": false, "boundElements": [], "updated": 1, @@ -291,7 +291,7 @@ "baseline": 14 }, { - "id": "t1664748787", + "id": "t1824576001", "type": "text", "x": 240, "y": 83, @@ -308,8 +308,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 522412668, - "versionNonce": 1556357731, + "seed": 1293174951, + "versionNonce": 1616100610, "isDeleted": false, "boundElements": [], "updated": 1, @@ -326,7 +326,7 @@ "baseline": 11 }, { - "id": "r1743319907", + "id": "r1258664448", "type": "rectangle", "x": 30, "y": 140, @@ -345,8 +345,8 @@ "roundness": { "type": 3 }, - "seed": 518703901, - "versionNonce": 1837843962, + "seed": 1960021414, + "versionNonce": 1416646457, "isDeleted": false, "boundElements": [], "updated": 1, @@ -354,11 +354,11 @@ "locked": false }, { - "id": "t1608031065", + "id": "t95345070", "type": "text", "x": 40, "y": 148, - "width": 231.00000000000003, + "width": 84.7, "height": 17.5, "angle": 0, "strokeColor": "#111111", @@ -371,29 +371,29 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1939839487, - "versionNonce": 792980914, + "seed": 1013118978, + "versionNonce": 1937747243, "isDeleted": false, "boundElements": [], "updated": 1, "link": null, "locked": false, - "text": "COPY *.csproj \u2192 dotnet restore", + "text": "COPY source", "fontSize": 14, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "COPY *.csproj \u2192 dotnet restore", + "originalText": "COPY source", "lineHeight": 1.25, "baseline": 14 }, { - "id": "t984299095", + "id": "t1954280277", "type": "text", "x": 40, "y": 168, - "width": 229.9, + "width": 272.25, "height": 13.75, "angle": 0, "strokeColor": "#555555", @@ -406,25 +406,25 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1698775008, - "versionNonce": 1678799348, + "seed": 1822643637, + "versionNonce": 1165501644, "isDeleted": false, "boundElements": [], "updated": 1, "link": null, "locked": false, - "text": "download NuGet packages (cached layer)", + "text": "NuGet package cache is a BuildKit cache mount", "fontSize": 11, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "download NuGet packages (cached layer)", + "originalText": "NuGet package cache is a BuildKit cache mount", "lineHeight": 1.25, "baseline": 11 }, { - "id": "r519873325", + "id": "r1320777724", "type": "rectangle", "x": 30, "y": 210, @@ -443,8 +443,8 @@ "roundness": { "type": 3 }, - "seed": 1910939141, - "versionNonce": 627939933, + "seed": 1883131511, + "versionNonce": 597927633, "isDeleted": false, "boundElements": [], "updated": 1, @@ -452,11 +452,11 @@ "locked": false }, { - "id": "t1067551494", + "id": "t1866880389", "type": "text", "x": 40, "y": 218, - "width": 300.3, + "width": 192.50000000000003, "height": 17.5, "angle": 0, "strokeColor": "#111111", @@ -469,29 +469,29 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 555058417, - "versionNonce": 335031322, + "seed": 1130222458, + "versionNonce": 82817994, "isDeleted": false, "boundElements": [], "updated": 1, "link": null, "locked": false, - "text": "COPY source \u2192 dotnet publish -c Release", + "text": "dotnet publish -c Release", "fontSize": 14, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "COPY source \u2192 dotnet publish -c Release", + "originalText": "dotnet publish -c Release", "lineHeight": 1.25, "baseline": 14 }, { - "id": "t1361589784", + "id": "t68393002", "type": "text", "x": 40, "y": 238, - "width": 199.65, + "width": 242.00000000000003, "height": 13.75, "angle": 0, "strokeColor": "#555555", @@ -504,25 +504,25 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 466038275, - "versionNonce": 1766518633, + "seed": 188569218, + "versionNonce": 1833783195, "isDeleted": false, "boundElements": [], "updated": 1, "link": null, "locked": false, - "text": "compile C# \u2192 CIL, tree-shake deps", + "text": "restore from the mount, compile C# \u2192 CIL", "fontSize": 11, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "compile C# \u2192 CIL, tree-shake deps", + "originalText": "restore from the mount, compile C# \u2192 CIL", "lineHeight": 1.25, "baseline": 11 }, { - "id": "r256857851", + "id": "r1429649279", "type": "rectangle", "x": 30, "y": 290, @@ -541,8 +541,8 @@ "roundness": { "type": 3 }, - "seed": 1785239898, - "versionNonce": 992785546, + "seed": 231370000, + "versionNonce": 439390764, "isDeleted": false, "boundElements": [], "updated": 1, @@ -550,7 +550,7 @@ "locked": false }, { - "id": "t545888851", + "id": "t1536481541", "type": "text", "x": 40, "y": 298, @@ -567,8 +567,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1327525133, - "versionNonce": 1399809911, + "seed": 1721214585, + "versionNonce": 1491859071, "isDeleted": false, "boundElements": [], "updated": 1, @@ -585,7 +585,7 @@ "baseline": 14 }, { - "id": "t1919834902", + "id": "t1006764884", "type": "text", "x": 40, "y": 318, @@ -602,8 +602,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1242398867, - "versionNonce": 378512413, + "seed": 1770097525, + "versionNonce": 1517767943, "isDeleted": false, "boundElements": [], "updated": 1, @@ -620,7 +620,7 @@ "baseline": 11 }, { - "id": "t588679310", + "id": "t1359925032", "type": "text", "x": 40, "y": 333, @@ -637,8 +637,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1586055838, - "versionNonce": 1536765927, + "seed": 367615761, + "versionNonce": 1437718169, "isDeleted": false, "boundElements": [], "updated": 1, @@ -655,7 +655,7 @@ "baseline": 11 }, { - "id": "r1613980246", + "id": "r499811694", "type": "rectangle", "x": 220, "y": 290, @@ -674,8 +674,8 @@ "roundness": { "type": 3 }, - "seed": 962617492, - "versionNonce": 1775900094, + "seed": 1627769190, + "versionNonce": 1820529785, "isDeleted": false, "boundElements": [], "updated": 1, @@ -683,7 +683,7 @@ "locked": false }, { - "id": "t720289888", + "id": "t240097139", "type": "text", "x": 230, "y": 298, @@ -700,8 +700,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 877155264, - "versionNonce": 1334990330, + "seed": 909867335, + "versionNonce": 523484098, "isDeleted": false, "boundElements": [], "updated": 1, @@ -718,7 +718,7 @@ "baseline": 14 }, { - "id": "t1640539718", + "id": "t761026956", "type": "text", "x": 230, "y": 318, @@ -735,8 +735,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 216466352, - "versionNonce": 1622917091, + "seed": 682816277, + "versionNonce": 45929392, "isDeleted": false, "boundElements": [], "updated": 1, @@ -753,7 +753,7 @@ "baseline": 11 }, { - "id": "t541587849", + "id": "t502650002", "type": "text", "x": 230, "y": 333, @@ -770,8 +770,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 913179418, - "versionNonce": 686420532, + "seed": 1830770504, + "versionNonce": 46165145, "isDeleted": false, "boundElements": [], "updated": 1, @@ -788,7 +788,7 @@ "baseline": 11 }, { - "id": "r1414948367", + "id": "r491038982", "type": "rectangle", "x": 450, "y": 55, @@ -807,8 +807,8 @@ "roundness": { "type": 3 }, - "seed": 1069667389, - "versionNonce": 742787301, + "seed": 1692743835, + "versionNonce": 582721581, "isDeleted": false, "boundElements": [], "updated": 1, @@ -816,7 +816,7 @@ "locked": false }, { - "id": "t1017037267", + "id": "t1090021694", "type": "text", "x": 460, "y": 63, @@ -833,8 +833,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1168275139, - "versionNonce": 593543014, + "seed": 933591999, + "versionNonce": 194836677, "isDeleted": false, "boundElements": [], "updated": 1, @@ -851,7 +851,7 @@ "baseline": 14 }, { - "id": "t123703408", + "id": "t1851085573", "type": "text", "x": 460, "y": 83, @@ -868,8 +868,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 426137188, - "versionNonce": 138905582, + "seed": 1734539334, + "versionNonce": 625361864, "isDeleted": false, "boundElements": [], "updated": 1, @@ -886,7 +886,7 @@ "baseline": 11 }, { - "id": "r1314723998", + "id": "r1058340271", "type": "rectangle", "x": 670, "y": 55, @@ -905,8 +905,8 @@ "roundness": { "type": 3 }, - "seed": 782110080, - "versionNonce": 1931012282, + "seed": 966999289, + "versionNonce": 335545683, "isDeleted": false, "boundElements": [], "updated": 1, @@ -914,7 +914,7 @@ "locked": false }, { - "id": "t51735691", + "id": "t1915821875", "type": "text", "x": 680, "y": 63, @@ -931,8 +931,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1106054194, - "versionNonce": 277774654, + "seed": 158315657, + "versionNonce": 57661641, "isDeleted": false, "boundElements": [], "updated": 1, @@ -949,7 +949,7 @@ "baseline": 14 }, { - "id": "t1455473474", + "id": "t386445043", "type": "text", "x": 680, "y": 83, @@ -966,8 +966,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 711039965, - "versionNonce": 755671789, + "seed": 1382836805, + "versionNonce": 10215180, "isDeleted": false, "boundElements": [], "updated": 1, @@ -984,7 +984,7 @@ "baseline": 11 }, { - "id": "r1683534568", + "id": "r1750091642", "type": "rectangle", "x": 450, "y": 140, @@ -1003,8 +1003,8 @@ "roundness": { "type": 3 }, - "seed": 1850107008, - "versionNonce": 1443767182, + "seed": 771058737, + "versionNonce": 1050954762, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1012,7 +1012,7 @@ "locked": false }, { - "id": "t445555010", + "id": "t937315120", "type": "text", "x": 460, "y": 148, @@ -1029,8 +1029,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 352371418, - "versionNonce": 856313368, + "seed": 170586211, + "versionNonce": 1686962056, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1047,7 +1047,7 @@ "baseline": 14 }, { - "id": "t1038684560", + "id": "t1013290817", "type": "text", "x": 460, "y": 168, @@ -1064,8 +1064,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1896716970, - "versionNonce": 735933387, + "seed": 755012138, + "versionNonce": 1055502734, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1082,7 +1082,7 @@ "baseline": 11 }, { - "id": "r328154780", + "id": "r185060290", "type": "rectangle", "x": 450, "y": 210, @@ -1101,8 +1101,8 @@ "roundness": { "type": 3 }, - "seed": 117829651, - "versionNonce": 648414238, + "seed": 640528641, + "versionNonce": 1946591229, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1110,7 +1110,7 @@ "locked": false }, { - "id": "t246427179", + "id": "t1382696477", "type": "text", "x": 460, "y": 218, @@ -1127,8 +1127,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 687070761, - "versionNonce": 940611493, + "seed": 1092324639, + "versionNonce": 945276022, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1145,7 +1145,7 @@ "baseline": 14 }, { - "id": "t377387168", + "id": "t1466539407", "type": "text", "x": 460, "y": 238, @@ -1162,8 +1162,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 222106449, - "versionNonce": 563770894, + "seed": 1921584813, + "versionNonce": 471850976, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1180,7 +1180,7 @@ "baseline": 11 }, { - "id": "r53525866", + "id": "r311780930", "type": "rectangle", "x": 670, "y": 210, @@ -1199,8 +1199,8 @@ "roundness": { "type": 3 }, - "seed": 699924137, - "versionNonce": 1536151409, + "seed": 1887114370, + "versionNonce": 1924205161, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1208,7 +1208,7 @@ "locked": false }, { - "id": "t1862482530", + "id": "t1340788997", "type": "text", "x": 680, "y": 218, @@ -1225,8 +1225,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 655257656, - "versionNonce": 26650080, + "seed": 601909602, + "versionNonce": 841576416, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1243,7 +1243,7 @@ "baseline": 14 }, { - "id": "t197035251", + "id": "t393377631", "type": "text", "x": 680, "y": 238, @@ -1260,8 +1260,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1736223589, - "versionNonce": 1318066861, + "seed": 1965482214, + "versionNonce": 1211167333, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1278,7 +1278,7 @@ "baseline": 11 }, { - "id": "r1196617887", + "id": "r708400717", "type": "rectangle", "x": 450, "y": 290, @@ -1297,8 +1297,8 @@ "roundness": { "type": 3 }, - "seed": 1022797140, - "versionNonce": 12086152, + "seed": 328142801, + "versionNonce": 522148282, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1306,7 +1306,7 @@ "locked": false }, { - "id": "t1621474659", + "id": "t1059396272", "type": "text", "x": 460, "y": 298, @@ -1323,8 +1323,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1311811912, - "versionNonce": 1010211352, + "seed": 1882258898, + "versionNonce": 1376900312, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1341,7 +1341,7 @@ "baseline": 14 }, { - "id": "t1817728479", + "id": "t1063521260", "type": "text", "x": 460, "y": 318, @@ -1358,8 +1358,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1593332083, - "versionNonce": 1299552206, + "seed": 570770361, + "versionNonce": 1276713519, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1376,7 +1376,7 @@ "baseline": 11 }, { - "id": "r1146706571", + "id": "r433264218", "type": "rectangle", "x": 450, "y": 370, @@ -1395,8 +1395,8 @@ "roundness": { "type": 3 }, - "seed": 574325143, - "versionNonce": 1846139663, + "seed": 1085036783, + "versionNonce": 923347225, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1404,7 +1404,7 @@ "locked": false }, { - "id": "t511671025", + "id": "t1016453900", "type": "text", "x": 460, "y": 378, @@ -1421,8 +1421,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1362196454, - "versionNonce": 1703632255, + "seed": 1282915497, + "versionNonce": 1073764052, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1439,7 +1439,7 @@ "baseline": 14 }, { - "id": "t1757927274", + "id": "t575799520", "type": "text", "x": 460, "y": 398, @@ -1456,8 +1456,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 425244794, - "versionNonce": 503134203, + "seed": 1154479314, + "versionNonce": 1161400344, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1474,7 +1474,7 @@ "baseline": 11 }, { - "id": "r211485186", + "id": "r1361555404", "type": "rectangle", "x": 660, "y": 370, @@ -1493,8 +1493,8 @@ "roundness": { "type": 3 }, - "seed": 748648635, - "versionNonce": 1570813620, + "seed": 424551950, + "versionNonce": 1180819606, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1502,7 +1502,7 @@ "locked": false }, { - "id": "t446120861", + "id": "t1987174204", "type": "text", "x": 670, "y": 378, @@ -1519,8 +1519,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 748087407, - "versionNonce": 1155255693, + "seed": 1911912221, + "versionNonce": 1527570093, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1537,7 +1537,7 @@ "baseline": 14 }, { - "id": "t393719273", + "id": "t1839943037", "type": "text", "x": 670, "y": 398, @@ -1554,8 +1554,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1766763431, - "versionNonce": 1104167635, + "seed": 1069937377, + "versionNonce": 1982694218, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1572,7 +1572,7 @@ "baseline": 11 }, { - "id": "a399429768", + "id": "a83016549", "type": "arrow", "x": 210, "y": 82, @@ -1591,8 +1591,8 @@ "roundness": { "type": 2 }, - "seed": 293506141, - "versionNonce": 1766886424, + "seed": 1964370576, + "versionNonce": 904067251, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1615,7 +1615,7 @@ "endArrowhead": "arrow" }, { - "id": "a1535049576", + "id": "a552472367", "type": "arrow", "x": 210, "y": 110, @@ -1634,8 +1634,8 @@ "roundness": { "type": 2 }, - "seed": 1141793586, - "versionNonce": 1965318978, + "seed": 984981715, + "versionNonce": 1065276872, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1658,7 +1658,7 @@ "endArrowhead": "arrow" }, { - "id": "t189919071", + "id": "t1892217232", "type": "text", "x": 210.0, "y": 111.0, @@ -1675,8 +1675,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 995406897, - "versionNonce": 124399710, + "seed": 1469779381, + "versionNonce": 424567064, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1693,7 +1693,7 @@ "baseline": 11 }, { - "id": "a944829365", + "id": "a1622461397", "type": "arrow", "x": 210, "y": 190, @@ -1712,8 +1712,8 @@ "roundness": { "type": 2 }, - "seed": 1596010797, - "versionNonce": 1563527809, + "seed": 779907170, + "versionNonce": 931001981, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1736,7 +1736,7 @@ "endArrowhead": "arrow" }, { - "id": "t673840347", + "id": "t1234638083", "type": "text", "x": 210.0, "y": 186.0, @@ -1753,8 +1753,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1996323264, - "versionNonce": 1878213615, + "seed": 942899844, + "versionNonce": 184322716, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1771,7 +1771,7 @@ "baseline": 11 }, { - "id": "a1303994576", + "id": "a1984862123", "type": "arrow", "x": 210, "y": 260, @@ -1790,8 +1790,8 @@ "roundness": { "type": 2 }, - "seed": 1324924868, - "versionNonce": 1251750017, + "seed": 715947730, + "versionNonce": 1311837298, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1814,7 +1814,7 @@ "endArrowhead": "arrow" }, { - "id": "a1523084542", + "id": "a1272370227", "type": "arrow", "x": 200, "y": 317, @@ -1833,8 +1833,8 @@ "roundness": { "type": 2 }, - "seed": 221750715, - "versionNonce": 929686245, + "seed": 525732084, + "versionNonce": 1515637836, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1857,7 +1857,7 @@ "endArrowhead": "arrow" }, { - "id": "t1104189471", + "id": "t880934080", "type": "text", "x": 325.0, "y": 227.0, @@ -1874,8 +1874,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 794939779, - "versionNonce": 1983826525, + "seed": 10142449, + "versionNonce": 1341972266, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1892,7 +1892,7 @@ "baseline": 11 }, { - "id": "a1225555821", + "id": "a1784280371", "type": "arrow", "x": 650, "y": 82, @@ -1911,8 +1911,8 @@ "roundness": { "type": 2 }, - "seed": 100420972, - "versionNonce": 228877094, + "seed": 966276002, + "versionNonce": 1937082008, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1935,7 +1935,7 @@ "endArrowhead": "arrow" }, { - "id": "a985577168", + "id": "a1637939336", "type": "arrow", "x": 650, "y": 190, @@ -1954,8 +1954,8 @@ "roundness": { "type": 2 }, - "seed": 1381521395, - "versionNonce": 1841067021, + "seed": 105762531, + "versionNonce": 189815949, "isDeleted": false, "boundElements": [], "updated": 1, @@ -1978,7 +1978,7 @@ "endArrowhead": "arrow" }, { - "id": "a640311748", + "id": "a1482916649", "type": "arrow", "x": 650, "y": 265, @@ -1997,8 +1997,8 @@ "roundness": { "type": 2 }, - "seed": 408977674, - "versionNonce": 844565375, + "seed": 1459782678, + "versionNonce": 627560243, "isDeleted": false, "boundElements": [], "updated": 1, @@ -2021,7 +2021,7 @@ "endArrowhead": "arrow" }, { - "id": "a1248159741", + "id": "a1780054564", "type": "arrow", "x": 545, "y": 345, @@ -2040,8 +2040,8 @@ "roundness": { "type": 2 }, - "seed": 754602311, - "versionNonce": 441455127, + "seed": 1431810827, + "versionNonce": 796881217, "isDeleted": false, "boundElements": [], "updated": 1, @@ -2064,7 +2064,7 @@ "endArrowhead": "arrow" }, { - "id": "a833106100", + "id": "a85841055", "type": "arrow", "x": 755, "y": 345, @@ -2083,8 +2083,8 @@ "roundness": { "type": 2 }, - "seed": 357570337, - "versionNonce": 766617943, + "seed": 1195070140, + "versionNonce": 577777392, "isDeleted": false, "boundElements": [], "updated": 1, @@ -2107,7 +2107,7 @@ "endArrowhead": "arrow" }, { - "id": "t1975600495", + "id": "t1253266842", "type": "text", "x": 30, "y": 365, @@ -2124,8 +2124,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 767496134, - "versionNonce": 1484401612, + "seed": 1122576885, + "versionNonce": 550761068, "isDeleted": false, "boundElements": [], "updated": 1, @@ -2142,7 +2142,7 @@ "baseline": 11 }, { - "id": "t29277879", + "id": "t1113912157", "type": "text", "x": 30, "y": 385, @@ -2159,8 +2159,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 85758078, - "versionNonce": 250462573, + "seed": 1945790840, + "versionNonce": 756066760, "isDeleted": false, "boundElements": [], "updated": 1, @@ -2177,7 +2177,7 @@ "baseline": 11 }, { - "id": "t855866122", + "id": "t710938620", "type": "text", "x": 30, "y": 415, @@ -2194,8 +2194,8 @@ "groupIds": [], "frameId": null, "roundness": null, - "seed": 1147758739, - "versionNonce": 1192612798, + "seed": 1596176491, + "versionNonce": 1873217476, "isDeleted": false, "boundElements": [], "updated": 1, diff --git a/assets/diagrams/29-container-layers.svg b/assets/diagrams/29-container-layers.svg index d4f6f746..3e2f338a 100644 --- a/assets/diagrams/29-container-layers.svg +++ b/assets/diagrams/29-container-layers.svg @@ -11,11 +11,11 @@ SDK tools MSBuild, NuGet, Roslyn -COPY *.csproj → dotnet restore -download NuGet packages (cached layer) +COPY source +NuGet package cache is a BuildKit cache mount -COPY source → dotnet publish -c Release -compile C# → CIL, tree-shake deps +dotnet publish -c Release +restore from the mount, compile C# → CIL /app output *.dll + deps diff --git a/assets/diagrams/31-minikube-image-flow.excalidraw b/assets/diagrams/31-minikube-image-flow.excalidraw new file mode 100644 index 00000000..66e9f72b --- /dev/null +++ b/assets/diagrams/31-minikube-image-flow.excalidraw @@ -0,0 +1,961 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1658414804", + "type": "rectangle", + "x": 10, + "y": 10, + "width": 540, + "height": 190, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1928397782, + "versionNonce": 573243402, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t595766937", + "type": "text", + "x": 22, + "y": 16, + "width": 40, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1346170216, + "versionNonce": 1860902883, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Host", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Host", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1784626649", + "type": "rectangle", + "x": 570, + "y": 10, + "width": 300, + "height": 190, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1233977956, + "versionNonce": 792795802, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t23035192", + "type": "text", + "x": 582, + "y": 16, + "width": 151.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 965278993, + "versionNonce": 286705343, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Node container cndp-k8s", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Node container cndp-k8s", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r732968658", + "type": "rectangle", + "x": 30, + "y": 55, + "width": 150, + "height": 55, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1462825714, + "versionNonce": 272727235, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t6724138", + "type": "text", + "x": 40, + "y": 63, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 975129696, + "versionNonce": 836818813, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Containerfile", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Containerfile", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1991131591", + "type": "text", + "x": 40, + "y": 83, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 683488411, + "versionNonce": 1124376095, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "per language, per service", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "per language, per service", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1597708064", + "type": "rectangle", + "x": 210, + "y": 55, + "width": 150, + "height": 55, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1363339514, + "versionNonce": 1590706397, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t194003059", + "type": "text", + "x": 220, + "y": 63, + "width": 92.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1999947334, + "versionNonce": 1481974815, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "docker build", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "docker build", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t443472228", + "type": "text", + "x": 220, + "y": 83, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1763519013, + "versionNonce": 955077189, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "host BuildKit cache", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "host BuildKit cache", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r625455821", + "type": "rectangle", + "x": 390, + "y": 55, + "width": 140, + "height": 55, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 307257532, + "versionNonce": 870013570, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t457342081", + "type": "text", + "x": 400, + "y": 63, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1124933595, + "versionNonce": 875990765, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Docker image store", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Docker image store", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t730000190", + "type": "text", + "x": 400, + "y": 83, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1891501507, + "versionNonce": 637691937, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "localhost/cndp-05/...", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "localhost/cndp-05/...", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1327860910", + "type": "rectangle", + "x": 590, + "y": 55, + "width": 130, + "height": 55, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1754608521, + "versionNonce": 1961479550, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1881808560", + "type": "text", + "x": 600, + "y": 63, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 413454364, + "versionNonce": 590978111, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "containerd", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "containerd", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t284460665", + "type": "text", + "x": 600, + "y": 83, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 191918122, + "versionNonce": 58225029, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "same image name", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "same image name", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1481425718", + "type": "rectangle", + "x": 740, + "y": 55, + "width": 110, + "height": 55, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1450282794, + "versionNonce": 1947643487, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1140071010", + "type": "text", + "x": 750, + "y": 63, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 511016156, + "versionNonce": 1079500045, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Pod", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Pod", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1329156524", + "type": "text", + "x": 750, + "y": 83, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 2358411, + "versionNonce": 581658638, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pullPolicy: Never", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pullPolicy: Never", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r298537015", + "type": "rectangle", + "x": 390, + "y": 140, + "width": 140, + "height": 40, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1514763079, + "versionNonce": 1496998814, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1883500553", + "type": "text", + "x": 400, + "y": 148, + "width": 146.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 7912317, + "versionNonce": 468653989, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tag :v1, no :latest", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tag :v1, no :latest", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r84932623", + "type": "rectangle", + "x": 30, + "y": 230, + "width": 820, + "height": 45, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 473531185, + "versionNonce": 1302109013, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1417806235", + "type": "text", + "x": 40, + "y": 238, + "width": 731.5000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 535050914, + "versionNonce": 1819058918, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Rejected: minikube image build (discards the host caches), a registry addon (extra moving part)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Rejected: minikube image build (discards the host caches), a registry addon (extra moving part)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "a1642280212", + "type": "arrow", + "x": 180, + "y": 82, + "width": 30, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 134121701, + "versionNonce": 959691525, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 30, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1896788659", + "type": "arrow", + "x": 360, + "y": 82, + "width": 30, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 121084940, + "versionNonce": 551848367, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 30, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1250254978", + "type": "arrow", + "x": 530, + "y": 82, + "width": 60, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 883710959, + "versionNonce": 691708791, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 60, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t913976957", + "type": "text", + "x": 560.0, + "y": 68.0, + "width": 60.50000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 906995246, + "versionNonce": 462960988, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "image load", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "image load", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a314838043", + "type": "arrow", + "x": 720, + "y": 82, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1482402584, + "versionNonce": 1895415792, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/31-minikube-image-flow.svg b/assets/diagrams/31-minikube-image-flow.svg new file mode 100644 index 00000000..42123dc4 --- /dev/null +++ b/assets/diagrams/31-minikube-image-flow.svg @@ -0,0 +1,32 @@ + + + +Host + +Node container cndp-k8s + +Containerfile +per language, per service + +docker build +host BuildKit cache + +Docker image store +localhost/cndp-05/... + +containerd +same image name + +Pod +pullPolicy: Never + +tag :v1, no :latest + +Rejected: minikube image build (discards the host caches), a registry addon (extra moving part) + + + +image load +image load + + \ No newline at end of file diff --git a/assets/diagrams/31-minikube-topology.excalidraw b/assets/diagrams/31-minikube-topology.excalidraw new file mode 100644 index 00000000..dce36bb5 --- /dev/null +++ b/assets/diagrams/31-minikube-topology.excalidraw @@ -0,0 +1,1818 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r763765463", + "type": "rectangle", + "x": 10, + "y": 10, + "width": 200, + "height": 400, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 350203311, + "versionNonce": 1466694133, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t101238522", + "type": "text", + "x": 22, + "y": 16, + "width": 132.0, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 734792494, + "versionNonce": 132567163, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Host (Docker Engine)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Host (Docker Engine)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r286678261", + "type": "rectangle", + "x": 230, + "y": 10, + "width": 640, + "height": 400, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f7f7f7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 865805093, + "versionNonce": 1916874634, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1392742089", + "type": "text", + "x": 242, + "y": 16, + "width": 369.6, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1316479979, + "versionNonce": 1626076239, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Node container cndp-k8s (containerd, Kubernetes v1.36.5)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Node container cndp-k8s (containerd, Kubernetes v1.36.5)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r663803124", + "type": "rectangle", + "x": 250, + "y": 40, + "width": 160, + "height": 360, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 923716424, + "versionNonce": 369266713, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t392208167", + "type": "text", + "x": 262, + "y": 46, + "width": 112.2, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 468500003, + "versionNonce": 906260725, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "namespace strimzi", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "namespace strimzi", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1575144450", + "type": "rectangle", + "x": 430, + "y": 40, + "width": 425, + "height": 360, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 691740941, + "versionNonce": 857762289, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t13324805", + "type": "text", + "x": 442, + "y": 46, + "width": 112.2, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1378214007, + "versionNonce": 1555228231, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "namespace cndp-05", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "namespace cndp-05", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r69408209", + "type": "rectangle", + "x": 25, + "y": 60, + "width": 170, + "height": 55, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 104979772, + "versionNonce": 1814302859, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t182217694", + "type": "text", + "x": 35, + "y": 68, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 193216460, + "versionNonce": 1052759060, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "curl, verify.sh", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "curl, verify.sh", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1969382581", + "type": "text", + "x": 35, + "y": 88, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1544634005, + "versionNonce": 879047460, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "127.0.0.1:30080", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "127.0.0.1:30080", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r54469213", + "type": "rectangle", + "x": 25, + "y": 135, + "width": 170, + "height": 55, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1446227092, + "versionNonce": 1201580410, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1133484818", + "type": "text", + "x": 35, + "y": 143, + "width": 53.900000000000006, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1491230627, + "versionNonce": 489007734, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Browser", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Browser", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1247903386", + "type": "text", + "x": 35, + "y": 163, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1557658204, + "versionNonce": 1910245910, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "127.0.0.1:30300", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "127.0.0.1:30300", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r816106984", + "type": "rectangle", + "x": 25, + "y": 210, + "width": 170, + "height": 55, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 749599074, + "versionNonce": 1189851303, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t892706223", + "type": "text", + "x": 35, + "y": 218, + "width": 130.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 766782525, + "versionNonce": 1320704609, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kubectl --context", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kubectl --context", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t944390999", + "type": "text", + "x": 35, + "y": 238, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 685466626, + "versionNonce": 1863396392, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cndp-k8s", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cndp-k8s", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1810888357", + "type": "rectangle", + "x": 25, + "y": 285, + "width": 170, + "height": 55, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 224722492, + "versionNonce": 723126090, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t254062122", + "type": "text", + "x": 35, + "y": 293, + "width": 107.80000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 126002096, + "versionNonce": 1993837751, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Other profiles", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Other profiles", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t130231333", + "type": "text", + "x": 35, + "y": 313, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 41482519, + "versionNonce": 1629998383, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stopped", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stopped", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1156654057", + "type": "rectangle", + "x": 265, + "y": 310, + "width": 130, + "height": 60, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 643327615, + "versionNonce": 1424279636, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1056810219", + "type": "text", + "x": 275, + "y": 318, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 980615940, + "versionNonce": 662049437, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Strimzi operator", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Strimzi operator", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1835050074", + "type": "text", + "x": 275, + "y": 338, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1099126777, + "versionNonce": 952692179, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Helm chart 1.2.0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Helm chart 1.2.0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r616357687", + "type": "rectangle", + "x": 450, + "y": 80, + "width": 190, + "height": 55, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1776599871, + "versionNonce": 752658949, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t703267393", + "type": "text", + "x": 460, + "y": 88, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 611791162, + "versionNonce": 1800034397, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "order-service", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "order-service", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t152253219", + "type": "text", + "x": 460, + "y": 108, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 330309310, + "versionNonce": 1659526392, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "NodePort 30080", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "NodePort 30080", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1047009093", + "type": "rectangle", + "x": 660, + "y": 80, + "width": 180, + "height": 55, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 215241140, + "versionNonce": 656038995, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1589823246", + "type": "text", + "x": 670, + "y": 88, + "width": 130.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1439605826, + "versionNonce": 1808455869, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "shipping-consumer", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "shipping-consumer", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1433444883", + "type": "text", + "x": 670, + "y": 108, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 583817742, + "versionNonce": 1214860143, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "notification-consumer", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "notification-consumer", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r353573967", + "type": "rectangle", + "x": 450, + "y": 180, + "width": 190, + "height": 60, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 925948611, + "versionNonce": 1606526492, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1670664918", + "type": "text", + "x": 460, + "y": 188, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 879945883, + "versionNonce": 572007824, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Kafka cndp (KRaft)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Kafka cndp (KRaft)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t84873210", + "type": "text", + "x": 460, + "y": 208, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 958480289, + "versionNonce": 1654791318, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cndp-kafka-bootstrap:9092", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cndp-kafka-bootstrap:9092", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1288837724", + "type": "rectangle", + "x": 660, + "y": 180, + "width": 180, + "height": 60, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 473714443, + "versionNonce": 67374638, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1367003199", + "type": "text", + "x": 670, + "y": 188, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 893486838, + "versionNonce": 1008241533, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Postgres 18", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Postgres 18", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t973687577", + "type": "text", + "x": 670, + "y": 208, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 128620457, + "versionNonce": 1787776480, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cndp-postgres-rw:5432", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cndp-postgres-rw:5432", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1154404639", + "type": "rectangle", + "x": 450, + "y": 285, + "width": 190, + "height": 60, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1380156553, + "versionNonce": 810888700, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t349891020", + "type": "text", + "x": 460, + "y": 293, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 553724933, + "versionNonce": 994588939, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "LGTM (otel-lgtm)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "LGTM (otel-lgtm)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t950607090", + "type": "text", + "x": 460, + "y": 313, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 261634559, + "versionNonce": 344140586, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "lgtm:4318, Grafana 30300", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "lgtm:4318, Grafana 30300", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r613443673", + "type": "rectangle", + "x": 660, + "y": 285, + "width": 180, + "height": 60, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1074166842, + "versionNonce": 1512919253, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t341283806", + "type": "text", + "x": 670, + "y": 293, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1459030092, + "versionNonce": 207117054, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Secret cndp-postgres-app", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Secret cndp-postgres-app", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t763787491", + "type": "text", + "x": 670, + "y": 313, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1992203973, + "versionNonce": 1987580168, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "PG_USER, PG_PASSWORD", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "PG_USER, PG_PASSWORD", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a4249934", + "type": "arrow", + "x": 195, + "y": 87, + "width": 255, + "height": 20, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 831720669, + "versionNonce": 709667630, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 255, + 20 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1862777686", + "type": "arrow", + "x": 195, + "y": 162, + "width": 255, + "height": 153, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1780759875, + "versionNonce": 1691636436, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 255, + 153 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1687426148", + "type": "arrow", + "x": 195, + "y": 237, + "width": 255, + "height": 22, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 930022048, + "versionNonce": 552677022, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 255, + -22 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1766193449", + "type": "text", + "x": 322.5, + "y": 212.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1119402605, + "versionNonce": 1272750046, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "exec", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "exec", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a422675005", + "type": "arrow", + "x": 395, + "y": 335, + "width": 55, + "height": 110, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1395556743, + "versionNonce": 1332159023, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 55, + -110 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1769046116", + "type": "text", + "x": 422.5, + "y": 266.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1060907543, + "versionNonce": 235409310, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "manages", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "manages", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1896311990", + "type": "arrow", + "x": 545, + "y": 135, + "width": 0, + "height": 45, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1191570307, + "versionNonce": 1326500642, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 45 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1340044017", + "type": "text", + "x": 545.0, + "y": 143.5, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 27917635, + "versionNonce": 391703832, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "produce", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "produce", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1893720800", + "type": "arrow", + "x": 750, + "y": 135, + "width": 0, + "height": 45, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 623160264, + "versionNonce": 1601395166, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 45 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t158685742", + "type": "text", + "x": 750.0, + "y": 143.5, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1169643267, + "versionNonce": 50099428, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "insert", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "insert", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1261815152", + "type": "arrow", + "x": 640, + "y": 208, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1551128096, + "versionNonce": 1464496415, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t583911732", + "type": "text", + "x": 25, + "y": 385, + "width": 671.5500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 402408348, + "versionNonce": 1556904106, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Both ports are published on 127.0.0.1 when the profile is created. No port-forward, tunnel or minikube service.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Both ports are published on 127.0.0.1 when the profile is created. No port-forward, tunnel or minikube service.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/31-minikube-topology.svg b/assets/diagrams/31-minikube-topology.svg new file mode 100644 index 00000000..e5c7c1c6 --- /dev/null +++ b/assets/diagrams/31-minikube-topology.svg @@ -0,0 +1,60 @@ + + + +Host (Docker Engine) + +Node container cndp-k8s (containerd, Kubernetes v1.36.5) + +namespace strimzi + +namespace cndp-05 + +curl, verify.sh +127.0.0.1:30080 + +Browser +127.0.0.1:30300 + +kubectl --context +cndp-k8s + +Other profiles +stopped + +Strimzi operator +Helm chart 1.2.0 + +order-service +NodePort 30080 + +shipping-consumer +notification-consumer + +Kafka cndp (KRaft) +cndp-kafka-bootstrap:9092 + +Postgres 18 +cndp-postgres-rw:5432 + +LGTM (otel-lgtm) +lgtm:4318, Grafana 30300 + +Secret cndp-postgres-app +PG_USER, PG_PASSWORD + + + +exec +exec + +manages +manages + +produce +produce + +insert +insert + +Both ports are published on 127.0.0.1 when the profile is created. No port-forward, tunnel or minikube service. + \ No newline at end of file diff --git a/examples/01-cloud-native-principles/dotnet/Containerfile b/examples/01-cloud-native-principles/dotnet/Containerfile index 0f957cb9..817abc21 100644 --- a/examples/01-cloud-native-principles/dotnet/Containerfile +++ b/examples/01-cloud-native-principles/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/02-communications/dotnet/Containerfile.inventory b/examples/02-communications/dotnet/Containerfile.inventory index 964b6ffe..8b674d12 100644 --- a/examples/02-communications/dotnet/Containerfile.inventory +++ b/examples/02-communications/dotnet/Containerfile.inventory @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 InventoryService/InventoryService.csproj InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore InventoryService/InventoryService.csproj COPY --chown=1001:0 InventoryService/ InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish InventoryService/InventoryService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish InventoryService/InventoryService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/02-communications/dotnet/Containerfile.order b/examples/02-communications/dotnet/Containerfile.order index 291ef64f..8e0a2b78 100644 --- a/examples/02-communications/dotnet/Containerfile.order +++ b/examples/02-communications/dotnet/Containerfile.order @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/02-communications/quarkus/inventory-service/src/main/resources/application.properties b/examples/02-communications/quarkus/inventory-service/src/main/resources/application.properties index a50ca24a..a77566b4 100644 --- a/examples/02-communications/quarkus/inventory-service/src/main/resources/application.properties +++ b/examples/02-communications/quarkus/inventory-service/src/main/resources/application.properties @@ -11,4 +11,5 @@ quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n # OpenTelemetry quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:inventory-service} diff --git a/examples/02-communications/quarkus/order-service/src/main/resources/application.properties b/examples/02-communications/quarkus/order-service/src/main/resources/application.properties index 251b0340..8b9044ce 100644 --- a/examples/02-communications/quarkus/order-service/src/main/resources/application.properties +++ b/examples/02-communications/quarkus/order-service/src/main/resources/application.properties @@ -27,4 +27,5 @@ quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n # OpenTelemetry quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:order-service} diff --git a/examples/03-composition/dotnet/Containerfile.gateway b/examples/03-composition/dotnet/Containerfile.gateway index a3c27521..a804e69b 100644 --- a/examples/03-composition/dotnet/Containerfile.gateway +++ b/examples/03-composition/dotnet/Containerfile.gateway @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 Gateway/Gateway.csproj Gateway/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore Gateway/Gateway.csproj COPY --chown=1001:0 Gateway/ Gateway/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish Gateway/Gateway.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish Gateway/Gateway.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/03-composition/dotnet/Containerfile.inventory b/examples/03-composition/dotnet/Containerfile.inventory index 964b6ffe..8b674d12 100644 --- a/examples/03-composition/dotnet/Containerfile.inventory +++ b/examples/03-composition/dotnet/Containerfile.inventory @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 InventoryService/InventoryService.csproj InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore InventoryService/InventoryService.csproj COPY --chown=1001:0 InventoryService/ InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish InventoryService/InventoryService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish InventoryService/InventoryService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/03-composition/dotnet/Containerfile.order-api b/examples/03-composition/dotnet/Containerfile.order-api index bd70e09f..257fcce7 100644 --- a/examples/03-composition/dotnet/Containerfile.order-api +++ b/examples/03-composition/dotnet/Containerfile.order-api @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderApi/OrderApi.csproj OrderApi/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderApi/OrderApi.csproj COPY --chown=1001:0 OrderApi/ OrderApi/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderApi/OrderApi.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderApi/OrderApi.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/03-composition/quarkus/gateway/src/main/resources/application.properties b/examples/03-composition/quarkus/gateway/src/main/resources/application.properties index c6ed493b..320139ee 100644 --- a/examples/03-composition/quarkus/gateway/src/main/resources/application.properties +++ b/examples/03-composition/quarkus/gateway/src/main/resources/application.properties @@ -12,4 +12,5 @@ quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] %s%e%n quarkus.otel.enabled=true quarkus.otel.exporter.otlp.endpoint=http://lgtm:4318 +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.otel.service.name=gateway diff --git a/examples/03-composition/quarkus/inventory-service/src/main/resources/application.properties b/examples/03-composition/quarkus/inventory-service/src/main/resources/application.properties index b3d2a456..a2bce44a 100644 --- a/examples/03-composition/quarkus/inventory-service/src/main/resources/application.properties +++ b/examples/03-composition/quarkus/inventory-service/src/main/resources/application.properties @@ -7,4 +7,5 @@ quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] %s%e%n quarkus.otel.enabled=true quarkus.otel.exporter.otlp.endpoint=http://lgtm:4318 +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.otel.service.name=inventory-service diff --git a/examples/03-composition/quarkus/order-api/src/main/resources/application.properties b/examples/03-composition/quarkus/order-api/src/main/resources/application.properties index 38096fe0..12539d5b 100644 --- a/examples/03-composition/quarkus/order-api/src/main/resources/application.properties +++ b/examples/03-composition/quarkus/order-api/src/main/resources/application.properties @@ -10,4 +10,5 @@ quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] %s%e%n quarkus.otel.enabled=true quarkus.otel.exporter.otlp.endpoint=http://lgtm:4318 +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.otel.service.name=order-api diff --git a/examples/04-data/dotnet/Containerfile b/examples/04-data/dotnet/Containerfile index 0f957cb9..817abc21 100644 --- a/examples/04-data/dotnet/Containerfile +++ b/examples/04-data/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/05-event-driven/README.md b/examples/05-event-driven/README.md index a4caf3dd..f10a3cff 100644 --- a/examples/05-event-driven/README.md +++ b/examples/05-event-driven/README.md @@ -70,6 +70,17 @@ docker exec cndp-kafka /opt/kafka/bin/kafka-consumer-groups.sh \ --bootstrap-server localhost:9092 --list ``` +## Run on minikube + +The same services and the same `verify.sh` run on a single-node minikube cluster (Docker +driver, Kubernetes v1.36.5) with Kafka managed by the Strimzi operator. See +[`k8s/README.md`](k8s/README.md) for the commands and Appendix R in the site for the design. + +```bash +k8s/setup-profile.sh && k8s/build-images.sh go && k8s/deploy.sh go +RUNTIME=kubernetes BASE=http://127.0.0.1:30080 ./verify.sh +``` + ## Verify From the example root (not the language directory): diff --git a/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt b/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt index bba01e33..2e40b9e6 100644 --- a/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt +++ b/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt @@ -9,10 +9,17 @@ add_compile_options(-Wall -Wextra) # gcc-toolset-15's libstdc++ is newer than the one in ubi-minimal: link it statically. add_link_options(-static-libstdc++ -static-libgcc) +# Static archives that depend on each other (librdkafka, OpenTelemetry): rescan the group. +set(CMAKE_CXX_LINK_EXECUTABLE + " \ + -o -Wl,--start-group -Wl,--end-group") + find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Conan find_package(jsoncpp CONFIG REQUIRED) find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client) +find_package(opentelemetry-cpp CONFIG REQUIRED) find_package(Threads REQUIRED) add_executable(notification-consumer src/main.cpp) -target_link_libraries(notification-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL Threads::Threads) +target_include_directories(notification-consumer PRIVATE src) +target_link_libraries(notification-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads) diff --git a/examples/05-event-driven/cpp/notification-consumer/README.md b/examples/05-event-driven/cpp/notification-consumer/README.md index 7a255353..fddf9f67 100644 --- a/examples/05-event-driven/cpp/notification-consumer/README.md +++ b/examples/05-event-driven/cpp/notification-consumer/README.md @@ -12,6 +12,8 @@ Consumes `order.placed` in consumer group `notification-group` and writes `notif - **Consumer**: `RdKafka::KafkaConsumer`, `enable.auto.commit=false`; the offset is committed (`commitSync`) only after the Postgres row is written (libpq, `ON CONFLICT DO NOTHING`), so a crash redelivers instead of losing the event. - **libpq 17.11** (direct, no libpqxx), **jsoncpp 1.9.8**. +- **Tracing**: OpenTelemetry C++ 1.26.0 (OTLP/HTTP to `OTEL_EXPORTER_OTLP_ENDPOINT`, service name `notification-consumer`). One consumer span per + record, parented on the `traceparent` header the order service sets. ## Build diff --git a/examples/05-event-driven/cpp/notification-consumer/conan.lock b/examples/05-event-driven/cpp/notification-consumer/conan.lock index c9f47f4f..cca59aef 100644 --- a/examples/05-event-driven/cpp/notification-consumer/conan.lock +++ b/examples/05-event-driven/cpp/notification-consumer/conan.lock @@ -2,25 +2,41 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", + "opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", + "nlohmann_json/3.12.0#2d634ab0ec8d9f56353e5ccef6d6612c%1744735883.94", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1775037240.923", "librdkafka/2.14.2#ebe1ce610f423ebe1dbfd52756b98db8%1781073238.939", "libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346", - "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522" + "libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861", + "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522", + "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "build_requires": [ + "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "pkgconf/2.5.1#93c2051284cba1279494a43a4fcfeae2%1757684701.089", + "opentelemetry-proto/1.7.0#ed6d5bd761bef0afb0ba09676420b9ea%1749461220.268", "ninja/1.13.2#c8c5dc2a52ed6e4e42a66d75b4717ceb%1764096931.974", "meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754", "m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659", + "libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847", "gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329", "flex/2.6.4#efa781fc5088b47c895bd4eef6911f2e%1761560242.855", - "bison/3.8.2#c3490cbe0078b6fd3eb4cf5ed64144dc%1755688988.644" + "cmake/4.4.4#fec9c7048fae91e03c31e59977cd9c12%1791276141.315", + "bison/3.8.2#c3490cbe0078b6fd3eb4cf5ed64144dc%1755688988.644", + "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56", + "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86", + "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "python_requires": [], "overrides": { "lz4/1.9.4": [ "lz4/1.10.0" + ], + "nlohmann_json/3.11.3": [ + "nlohmann_json/3.12.0" ] }, "config_requires": [] diff --git a/examples/05-event-driven/cpp/notification-consumer/conanfile.py b/examples/05-event-driven/cpp/notification-consumer/conanfile.py index 0caf2120..82483aec 100644 --- a/examples/05-event-driven/cpp/notification-consumer/conanfile.py +++ b/examples/05-event-driven/cpp/notification-consumer/conanfile.py @@ -18,4 +18,7 @@ def requirements(self): self.requires("libpq/17.11") self.requires("librdkafka/2.14.2") self.requires("jsoncpp/1.9.8") + self.requires("opentelemetry-cpp/1.26.0") + # opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release. + self.requires("nlohmann_json/3.12.0", override=True) self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4 diff --git a/examples/05-event-driven/cpp/notification-consumer/src/main.cpp b/examples/05-event-driven/cpp/notification-consumer/src/main.cpp index ece7fd46..fdc4078b 100644 --- a/examples/05-event-driven/cpp/notification-consumer/src/main.cpp +++ b/examples/05-event-driven/cpp/notification-consumer/src/main.cpp @@ -17,6 +17,8 @@ #include #include +#include "telemetry.hpp" + namespace { std::atomic g_shutdown{false}; @@ -91,6 +93,9 @@ int main() { return 1; } + const char* svc = std::getenv("OTEL_SERVICE_NAME"); + cndp::telemetry::init(svc ? svc : "notification-consumer"); + const char* bootstrap = std::getenv("KAFKA_BOOTSTRAP"); if (!bootstrap) bootstrap = "kafka:9094"; @@ -128,10 +133,29 @@ int main() { std::string event(static_cast(msg->payload()), msg->len()); std::cout << "Received event: " << event << "\n"; + std::string traceparent; + if (auto* hdrs = msg->headers()) { + auto h = hdrs->get_last("traceparent"); + if (h.err() == RdKafka::ERR_NO_ERROR && h.value_string() != nullptr) + traceparent.assign(static_cast(h.value()), h.value_size()); + } + std::string order_id; + { + Json::Value root; + Json::CharReaderBuilder b; + std::string errs; + std::istringstream is(event); + if (Json::parseFromStream(b, is, &root, &errs) && root.isObject()) + order_id = root.get("id", "").asString(); + } + auto span = cndp::telemetry::start_consumer_span( + "notification-consumer", "order.placed process", traceparent, order_id); + Outcome outcome; while ((outcome = process_event(event)) == Outcome::Retry && !g_shutdown) { std::this_thread::sleep_for(std::chrono::seconds(1)); } + span->End(); if (outcome == Outcome::Retry) break; // shutting down: leave the offset uncommitted // Commit only after the side effect, so a crash redelivers rather than loses. if (RdKafka::ErrorCode err = consumer->commitSync(msg.get()); err != RdKafka::ERR_NO_ERROR) { @@ -154,6 +178,7 @@ int main() { RdKafka::wait_destroyed(5000); if (g_pg_conn) PQfinish(g_pg_conn); + cndp::telemetry::shutdown(); std::cout << "Notification consumer shutdown complete\n"; return 0; } diff --git a/examples/05-event-driven/cpp/notification-consumer/src/telemetry.hpp b/examples/05-event-driven/cpp/notification-consumer/src/telemetry.hpp new file mode 100644 index 00000000..68a3640a --- /dev/null +++ b/examples/05-event-driven/cpp/notification-consumer/src/telemetry.hpp @@ -0,0 +1,113 @@ +// telemetry.hpp — OpenTelemetry C++ traces over OTLP/HTTP. +// +// The exporters read OTEL_EXPORTER_OTLP_ENDPOINT (http://lgtm:4318 in compose; they append +// /v1/traces). The resource takes service.name from OTEL_SERVICE_NAME and +// OTEL_RESOURCE_ATTRIBUTES, with the name passed to init() as the default. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/sdk/trace/batch_span_processor_factory.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/sdk/trace/tracer_provider_factory.h" +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/span.h" + +namespace cndp::telemetry { + +namespace otel = opentelemetry; + +inline void init(const std::string& default_service_name) { + auto resource = otel::sdk::resource::Resource::Create({{"service.name", default_service_name}}); + resource = resource.Merge(otel::sdk::resource::OTELResourceDetector().Detect()); + + // Traces: batch processor -> OTLP/HTTP. + otel::exporter::otlp::OtlpHttpExporterOptions topts; + auto texporter = otel::exporter::otlp::OtlpHttpExporterFactory::Create(topts); + auto processor = otel::sdk::trace::BatchSpanProcessorFactory::Create( + std::move(texporter), otel::sdk::trace::BatchSpanProcessorOptions{}); + std::shared_ptr tp = + otel::sdk::trace::TracerProviderFactory::Create(std::move(processor), resource); + otel::trace::Provider::SetTracerProvider(tp); +} + +inline void shutdown() { + if (auto* tp = dynamic_cast( + otel::trace::Provider::GetTracerProvider().get())) { + tp->ForceFlush(); + tp->Shutdown(); + } +} + +using SpanPtr = otel::nostd::shared_ptr; + +inline otel::nostd::shared_ptr tracer(const std::string& name) { + return otel::trace::Provider::GetTracerProvider()->GetTracer(name); +} + +inline std::string trace_id_of(const SpanPtr& span) { + char buf[32]; + span->GetContext().trace_id().ToLowerBase16(buf); + return std::string(buf, 32); +} + +// W3C traceparent header value for a span (sampled). +inline std::string traceparent_of(const SpanPtr& span) { + char tid[32], sid[16]; + span->GetContext().trace_id().ToLowerBase16(tid); + span->GetContext().span_id().ToLowerBase16(sid); + return "00-" + std::string(tid, 32) + "-" + std::string(sid, 16) + "-01"; +} + +// Remote parent span context from a W3C traceparent header (00-<32 hex>-<16 hex>-<2 hex>). +inline bool parse_traceparent(const std::string& tp, otel::trace::SpanContext& out) { + auto nibble = [](char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; + }; + auto hex = [&](size_t off, uint8_t* dst, size_t n) { + for (size_t i = 0; i < n; ++i) { + int hi = nibble(tp[off + 2 * i]), lo = nibble(tp[off + 2 * i + 1]); + if (hi < 0 || lo < 0) return false; + dst[i] = static_cast(hi << 4 | lo); + } + return true; + }; + if (tp.size() < 55 || tp[2] != '-' || tp[35] != '-' || tp[52] != '-') return false; + uint8_t tid[16], sid[8]; + if (!hex(3, tid, 16) || !hex(36, sid, 8)) return false; + out = otel::trace::SpanContext( + otel::trace::TraceId(otel::nostd::span(tid)), + otel::trace::SpanId(otel::nostd::span(sid)), + otel::trace::TraceFlags(1), true); + return true; +} + +// Consumer span for one Kafka record, a child of the producer's span when the record carries a +// valid traceparent header and a new root otherwise. +inline SpanPtr start_consumer_span(const std::string& tracer_name, const std::string& span_name, + const std::string& traceparent, const std::string& order_id) { + otel::trace::StartSpanOptions so; + so.kind = otel::trace::SpanKind::kConsumer; + otel::trace::SpanContext parent = otel::trace::SpanContext::GetInvalid(); + if (parse_traceparent(traceparent, parent)) so.parent = parent; + return tracer(tracer_name)->StartSpan( + span_name, + {{"order.id", order_id}, {"messaging.system", "kafka"}, + {"messaging.destination.name", "order.placed"}}, + so); +} + +} // namespace cndp::telemetry diff --git a/examples/05-event-driven/cpp/order-service/CMakeLists.txt b/examples/05-event-driven/cpp/order-service/CMakeLists.txt index c7dffebe..9baa0495 100644 --- a/examples/05-event-driven/cpp/order-service/CMakeLists.txt +++ b/examples/05-event-driven/cpp/order-service/CMakeLists.txt @@ -9,14 +9,19 @@ add_compile_options(-Wall -Wextra) # gcc-toolset-15's libstdc++ is newer than the one in ubi-minimal: link it statically. add_link_options(-static-libstdc++ -static-libgcc) -# Static archives that depend on each other: let the linker rescan the group. +# Static archives that depend on each other (Drogon, librdkafka, OpenTelemetry): let the linker rescan the group. set(CMAKE_CXX_LINK_EXECUTABLE " -o -Wl,--start-group -Wl,--end-group") find_package(Drogon CONFIG REQUIRED) find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Conan find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client) +find_package(spdlog CONFIG REQUIRED) +find_package(opentelemetry-cpp CONFIG REQUIRED) find_package(Threads REQUIRED) add_executable(order-service src/main.cpp) -target_link_libraries(order-service PRIVATE Drogon::Drogon RdKafka::rdkafka++ PostgreSQL::PostgreSQL Threads::Threads) +target_include_directories(order-service PRIVATE src) +target_link_libraries(order-service PRIVATE + Drogon::Drogon RdKafka::rdkafka++ PostgreSQL::PostgreSQL spdlog::spdlog + opentelemetry-cpp::opentelemetry-cpp Threads::Threads) diff --git a/examples/05-event-driven/cpp/order-service/README.md b/examples/05-event-driven/cpp/order-service/README.md index 784d0779..3d2e8dfb 100644 --- a/examples/05-event-driven/cpp/order-service/README.md +++ b/examples/05-event-driven/cpp/order-service/README.md @@ -12,6 +12,9 @@ POST/GET `/orders`: inserts the order with libpq and publishes `order.placed`. - **Drogon 1.9.13** (REST), **libpq 17.11** (direct, no libpqxx). - **Producer**: `RdKafka::Producer` with `acks=all` and a `DeliveryReportCb`; a poller thread serves delivery reports, and shutdown calls `flush()` so queued events are delivered before exit. +- **Tracing**: OpenTelemetry C++ 1.26.0 (OTLP/HTTP to `OTEL_EXPORTER_OTLP_ENDPOINT`, service name `order-service`) with spdlog 1.17.0 for + trace-correlated logs. A server span covers `POST /orders`, a child producer span covers the publish, and the producer span goes out as a + W3C `traceparent` Kafka header. ## Build diff --git a/examples/05-event-driven/cpp/order-service/conan.lock b/examples/05-event-driven/cpp/order-service/conan.lock index 4ff35a99..5420a78c 100644 --- a/examples/05-event-driven/cpp/order-service/conan.lock +++ b/examples/05-event-driven/cpp/order-service/conan.lock @@ -4,22 +4,37 @@ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", "util-linux-libuuid/2.41.2#3ba347c98172dadfe417700cf399adac%1781172622.732", "trantor/1.5.28#76c9f6691d885fb26e60b3d68890375d%1785222775.93", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", + "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", + "opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", + "nlohmann_json/3.12.0#2d634ab0ec8d9f56353e5ccef6d6612c%1744735883.94", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1775037240.923", "librdkafka/2.14.2#ebe1ce610f423ebe1dbfd52756b98db8%1781073238.939", "libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346", + "libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861", "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522", + "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386", "drogon/1.9.13#251680a44562cf10050d53c0259876d5%1778471907.643", - "c-ares/1.34.8#074b97a9e38159813ed5e2dcfeb290ca%1784539118.141" + "c-ares/1.34.8#074b97a9e38159813ed5e2dcfeb290ca%1784539118.141", + "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "build_requires": [ + "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "pkgconf/2.5.1#93c2051284cba1279494a43a4fcfeae2%1757684701.089", + "opentelemetry-proto/1.7.0#ed6d5bd761bef0afb0ba09676420b9ea%1749461220.268", "ninja/1.13.2#c8c5dc2a52ed6e4e42a66d75b4717ceb%1764096931.974", "meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754", "m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659", + "libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847", "gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329", "flex/2.6.4#efa781fc5088b47c895bd4eef6911f2e%1761560242.855", - "bison/3.8.2#c3490cbe0078b6fd3eb4cf5ed64144dc%1755688988.644" + "cmake/4.4.4#fec9c7048fae91e03c31e59977cd9c12%1791276141.315", + "bison/3.8.2#c3490cbe0078b6fd3eb4cf5ed64144dc%1755688988.644", + "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56", + "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86", + "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "python_requires": [], "overrides": { @@ -31,6 +46,12 @@ ], "lz4/1.9.4": [ "lz4/1.10.0" + ], + "fmt/12.1.0": [ + "fmt/12.2.0" + ], + "nlohmann_json/3.11.3": [ + "nlohmann_json/3.12.0" ] }, "config_requires": [] diff --git a/examples/05-event-driven/cpp/order-service/conanfile.py b/examples/05-event-driven/cpp/order-service/conanfile.py index 5ac434a8..ef26fd80 100644 --- a/examples/05-event-driven/cpp/order-service/conanfile.py +++ b/examples/05-event-driven/cpp/order-service/conanfile.py @@ -19,6 +19,11 @@ def requirements(self): self.requires("drogon/1.9.13") self.requires("libpq/17.11") self.requires("librdkafka/2.14.2") + self.requires("spdlog/1.17.0") + self.requires("opentelemetry-cpp/1.26.0") + # opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release. + self.requires("nlohmann_json/3.12.0", override=True) + self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0 self.requires("jsoncpp/1.9.8", override=True) # drogon pins jsoncpp 1.9.5 self.requires("util-linux-libuuid/2.41.2", override=True) # drogon pins util-linux-libuuid 2.39.2 self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4 diff --git a/examples/05-event-driven/cpp/order-service/src/main.cpp b/examples/05-event-driven/cpp/order-service/src/main.cpp index 9f26222a..73be72f0 100644 --- a/examples/05-event-driven/cpp/order-service/src/main.cpp +++ b/examples/05-event-driven/cpp/order-service/src/main.cpp @@ -10,6 +10,10 @@ #include #include +#include + +#include "telemetry.hpp" + using namespace drogon; std::atomic g_shutdown{false}; @@ -106,6 +110,11 @@ int main() { std::signal(SIGTERM, signal_handler); std::signal(SIGINT, signal_handler); + { + const char* svc = std::getenv("OTEL_SERVICE_NAME"); + cndp::telemetry::init(svc ? svc : "order-service"); + } + try { init_pg(); init_kafka(); @@ -132,6 +141,18 @@ int main() { app().registerHandler("/orders", [](const HttpRequestPtr& req, std::function&& callback) { try { + auto tracer = cndp::telemetry::tracer("order-service"); + opentelemetry::trace::StartSpanOptions server_opts; + server_opts.kind = opentelemetry::trace::SpanKind::kServer; + cndp::telemetry::SpanPtr root = tracer->StartSpan( + "POST /orders", {{"http.request.method", "POST"}, {"url.path", "/orders"}}, + server_opts); + // Every return below ends the request span. + struct SpanGuard { + cndp::telemetry::SpanPtr& s; + ~SpanGuard() { s->End(); } + } root_guard{root}; + auto json = req->getJsonObject(); if (!json || !json->isMember("sku") || !json->isMember("quantity")) { auto resp = HttpResponse::newHttpResponse(); @@ -166,7 +187,14 @@ int main() { PQclear(res); pg_lock.unlock(); - // Publish to Kafka + // Publish to Kafka: producer span, its context travels as a traceparent header. + opentelemetry::trace::StartSpanOptions producer_opts; + producer_opts.kind = opentelemetry::trace::SpanKind::kProducer; + producer_opts.parent = root->GetContext(); + cndp::telemetry::SpanPtr pub = tracer->StartSpan( + "order.placed publish", + {{"messaging.system", "kafka"}, {"messaging.destination.name", "order.placed"}}, + producer_opts); std::ostringstream event; event << "{\"id\":\"" << json_escape(order_id) << "\"," << "\"sku\":\"" << json_escape(sku) << "\"," @@ -175,11 +203,16 @@ int main() { std::string event_str = event.str(); // Enqueue; the delivery report callback logs the outcome. + RdKafka::Headers* headers = RdKafka::Headers::create(); + headers->add("traceparent", cndp::telemetry::traceparent_of(pub)); RdKafka::ErrorCode perr = g_producer->produce( "order.placed", RdKafka::Topic::PARTITION_UA, RdKafka::Producer::RK_MSG_COPY, - event_str.data(), event_str.size(), nullptr, 0, 0, nullptr); + event_str.data(), event_str.size(), nullptr, 0, 0, headers, nullptr); if (perr != RdKafka::ERR_NO_ERROR) { - LOG_ERROR << "Kafka send failed: " << RdKafka::err2str(perr); + delete headers; // librdkafka owns the headers only when produce() succeeds + pub->SetStatus(opentelemetry::trace::StatusCode::kError, "produce failed"); + pub->End(); + spdlog::error("Kafka send failed: {}", RdKafka::err2str(perr)); // The event was not even queued: do not report the order as placed. Json::Value err; err["error"] = "event not published"; @@ -189,6 +222,11 @@ int main() { return; } + pub->End(); + root->SetAttribute("order.id", order_id); + spdlog::info("order placed id={} sku={} trace_id={}", order_id, sku, + cndp::telemetry::trace_id_of(root)); + // Return response Json::Value resp_json; resp_json["id"] = order_id; @@ -249,5 +287,6 @@ int main() { if (g_poller.joinable()) g_poller.join(); g_producer.reset(); if (g_pg_conn) PQfinish(g_pg_conn); + cndp::telemetry::shutdown(); return 0; } diff --git a/examples/05-event-driven/cpp/order-service/src/telemetry.hpp b/examples/05-event-driven/cpp/order-service/src/telemetry.hpp new file mode 100644 index 00000000..68a3640a --- /dev/null +++ b/examples/05-event-driven/cpp/order-service/src/telemetry.hpp @@ -0,0 +1,113 @@ +// telemetry.hpp — OpenTelemetry C++ traces over OTLP/HTTP. +// +// The exporters read OTEL_EXPORTER_OTLP_ENDPOINT (http://lgtm:4318 in compose; they append +// /v1/traces). The resource takes service.name from OTEL_SERVICE_NAME and +// OTEL_RESOURCE_ATTRIBUTES, with the name passed to init() as the default. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/sdk/trace/batch_span_processor_factory.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/sdk/trace/tracer_provider_factory.h" +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/span.h" + +namespace cndp::telemetry { + +namespace otel = opentelemetry; + +inline void init(const std::string& default_service_name) { + auto resource = otel::sdk::resource::Resource::Create({{"service.name", default_service_name}}); + resource = resource.Merge(otel::sdk::resource::OTELResourceDetector().Detect()); + + // Traces: batch processor -> OTLP/HTTP. + otel::exporter::otlp::OtlpHttpExporterOptions topts; + auto texporter = otel::exporter::otlp::OtlpHttpExporterFactory::Create(topts); + auto processor = otel::sdk::trace::BatchSpanProcessorFactory::Create( + std::move(texporter), otel::sdk::trace::BatchSpanProcessorOptions{}); + std::shared_ptr tp = + otel::sdk::trace::TracerProviderFactory::Create(std::move(processor), resource); + otel::trace::Provider::SetTracerProvider(tp); +} + +inline void shutdown() { + if (auto* tp = dynamic_cast( + otel::trace::Provider::GetTracerProvider().get())) { + tp->ForceFlush(); + tp->Shutdown(); + } +} + +using SpanPtr = otel::nostd::shared_ptr; + +inline otel::nostd::shared_ptr tracer(const std::string& name) { + return otel::trace::Provider::GetTracerProvider()->GetTracer(name); +} + +inline std::string trace_id_of(const SpanPtr& span) { + char buf[32]; + span->GetContext().trace_id().ToLowerBase16(buf); + return std::string(buf, 32); +} + +// W3C traceparent header value for a span (sampled). +inline std::string traceparent_of(const SpanPtr& span) { + char tid[32], sid[16]; + span->GetContext().trace_id().ToLowerBase16(tid); + span->GetContext().span_id().ToLowerBase16(sid); + return "00-" + std::string(tid, 32) + "-" + std::string(sid, 16) + "-01"; +} + +// Remote parent span context from a W3C traceparent header (00-<32 hex>-<16 hex>-<2 hex>). +inline bool parse_traceparent(const std::string& tp, otel::trace::SpanContext& out) { + auto nibble = [](char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; + }; + auto hex = [&](size_t off, uint8_t* dst, size_t n) { + for (size_t i = 0; i < n; ++i) { + int hi = nibble(tp[off + 2 * i]), lo = nibble(tp[off + 2 * i + 1]); + if (hi < 0 || lo < 0) return false; + dst[i] = static_cast(hi << 4 | lo); + } + return true; + }; + if (tp.size() < 55 || tp[2] != '-' || tp[35] != '-' || tp[52] != '-') return false; + uint8_t tid[16], sid[8]; + if (!hex(3, tid, 16) || !hex(36, sid, 8)) return false; + out = otel::trace::SpanContext( + otel::trace::TraceId(otel::nostd::span(tid)), + otel::trace::SpanId(otel::nostd::span(sid)), + otel::trace::TraceFlags(1), true); + return true; +} + +// Consumer span for one Kafka record, a child of the producer's span when the record carries a +// valid traceparent header and a new root otherwise. +inline SpanPtr start_consumer_span(const std::string& tracer_name, const std::string& span_name, + const std::string& traceparent, const std::string& order_id) { + otel::trace::StartSpanOptions so; + so.kind = otel::trace::SpanKind::kConsumer; + otel::trace::SpanContext parent = otel::trace::SpanContext::GetInvalid(); + if (parse_traceparent(traceparent, parent)) so.parent = parent; + return tracer(tracer_name)->StartSpan( + span_name, + {{"order.id", order_id}, {"messaging.system", "kafka"}, + {"messaging.destination.name", "order.placed"}}, + so); +} + +} // namespace cndp::telemetry diff --git a/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt b/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt index ef2e2241..63c9c71c 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt +++ b/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt @@ -9,10 +9,17 @@ add_compile_options(-Wall -Wextra) # gcc-toolset-15's libstdc++ is newer than the one in ubi-minimal: link it statically. add_link_options(-static-libstdc++ -static-libgcc) +# Static archives that depend on each other (librdkafka, OpenTelemetry): rescan the group. +set(CMAKE_CXX_LINK_EXECUTABLE + " \ + -o -Wl,--start-group -Wl,--end-group") + find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Conan find_package(jsoncpp CONFIG REQUIRED) find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client) +find_package(opentelemetry-cpp CONFIG REQUIRED) find_package(Threads REQUIRED) add_executable(shipping-consumer src/main.cpp) -target_link_libraries(shipping-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL Threads::Threads) +target_include_directories(shipping-consumer PRIVATE src) +target_link_libraries(shipping-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads) diff --git a/examples/05-event-driven/cpp/shipping-consumer/README.md b/examples/05-event-driven/cpp/shipping-consumer/README.md index 76864aef..a8d63895 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/README.md +++ b/examples/05-event-driven/cpp/shipping-consumer/README.md @@ -12,6 +12,8 @@ Consumes `order.placed` in consumer group `shipping-group` and writes `shipments - **Consumer**: `RdKafka::KafkaConsumer`, `enable.auto.commit=false`; the offset is committed (`commitSync`) only after the Postgres row is written (libpq, `ON CONFLICT DO NOTHING`), so a crash redelivers instead of losing the event. - **libpq 17.11** (direct, no libpqxx), **jsoncpp 1.9.8**. +- **Tracing**: OpenTelemetry C++ 1.26.0 (OTLP/HTTP to `OTEL_EXPORTER_OTLP_ENDPOINT`, service name `shipping-consumer`). One consumer span per + record, parented on the `traceparent` header the order service sets. ## Build diff --git a/examples/05-event-driven/cpp/shipping-consumer/conan.lock b/examples/05-event-driven/cpp/shipping-consumer/conan.lock index c9f47f4f..cca59aef 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/conan.lock +++ b/examples/05-event-driven/cpp/shipping-consumer/conan.lock @@ -2,25 +2,41 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", + "opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", + "nlohmann_json/3.12.0#2d634ab0ec8d9f56353e5ccef6d6612c%1744735883.94", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1775037240.923", "librdkafka/2.14.2#ebe1ce610f423ebe1dbfd52756b98db8%1781073238.939", "libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346", - "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522" + "libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861", + "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522", + "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "build_requires": [ + "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "pkgconf/2.5.1#93c2051284cba1279494a43a4fcfeae2%1757684701.089", + "opentelemetry-proto/1.7.0#ed6d5bd761bef0afb0ba09676420b9ea%1749461220.268", "ninja/1.13.2#c8c5dc2a52ed6e4e42a66d75b4717ceb%1764096931.974", "meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754", "m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659", + "libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847", "gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329", "flex/2.6.4#efa781fc5088b47c895bd4eef6911f2e%1761560242.855", - "bison/3.8.2#c3490cbe0078b6fd3eb4cf5ed64144dc%1755688988.644" + "cmake/4.4.4#fec9c7048fae91e03c31e59977cd9c12%1791276141.315", + "bison/3.8.2#c3490cbe0078b6fd3eb4cf5ed64144dc%1755688988.644", + "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56", + "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86", + "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "python_requires": [], "overrides": { "lz4/1.9.4": [ "lz4/1.10.0" + ], + "nlohmann_json/3.11.3": [ + "nlohmann_json/3.12.0" ] }, "config_requires": [] diff --git a/examples/05-event-driven/cpp/shipping-consumer/conanfile.py b/examples/05-event-driven/cpp/shipping-consumer/conanfile.py index a4f49a08..05c8c1ca 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/conanfile.py +++ b/examples/05-event-driven/cpp/shipping-consumer/conanfile.py @@ -18,4 +18,7 @@ def requirements(self): self.requires("libpq/17.11") self.requires("librdkafka/2.14.2") self.requires("jsoncpp/1.9.8") + self.requires("opentelemetry-cpp/1.26.0") + # opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release. + self.requires("nlohmann_json/3.12.0", override=True) self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4 diff --git a/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp b/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp index 4e0d8af1..daa8b81b 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp +++ b/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp @@ -17,6 +17,8 @@ #include #include +#include "telemetry.hpp" + namespace { std::atomic g_shutdown{false}; @@ -91,6 +93,9 @@ int main() { return 1; } + const char* svc = std::getenv("OTEL_SERVICE_NAME"); + cndp::telemetry::init(svc ? svc : "shipping-consumer"); + const char* bootstrap = std::getenv("KAFKA_BOOTSTRAP"); if (!bootstrap) bootstrap = "kafka:9094"; @@ -128,10 +133,29 @@ int main() { std::string event(static_cast(msg->payload()), msg->len()); std::cout << "Received event: " << event << "\n"; + std::string traceparent; + if (auto* hdrs = msg->headers()) { + auto h = hdrs->get_last("traceparent"); + if (h.err() == RdKafka::ERR_NO_ERROR && h.value_string() != nullptr) + traceparent.assign(static_cast(h.value()), h.value_size()); + } + std::string order_id; + { + Json::Value root; + Json::CharReaderBuilder b; + std::string errs; + std::istringstream is(event); + if (Json::parseFromStream(b, is, &root, &errs) && root.isObject()) + order_id = root.get("id", "").asString(); + } + auto span = cndp::telemetry::start_consumer_span( + "shipping-consumer", "order.placed process", traceparent, order_id); + Outcome outcome; while ((outcome = process_event(event)) == Outcome::Retry && !g_shutdown) { std::this_thread::sleep_for(std::chrono::seconds(1)); } + span->End(); if (outcome == Outcome::Retry) break; // shutting down: leave the offset uncommitted // Commit only after the side effect, so a crash redelivers rather than loses. if (RdKafka::ErrorCode err = consumer->commitSync(msg.get()); err != RdKafka::ERR_NO_ERROR) { @@ -154,6 +178,7 @@ int main() { RdKafka::wait_destroyed(5000); if (g_pg_conn) PQfinish(g_pg_conn); + cndp::telemetry::shutdown(); std::cout << "Shipping consumer shutdown complete\n"; return 0; } diff --git a/examples/05-event-driven/cpp/shipping-consumer/src/telemetry.hpp b/examples/05-event-driven/cpp/shipping-consumer/src/telemetry.hpp new file mode 100644 index 00000000..68a3640a --- /dev/null +++ b/examples/05-event-driven/cpp/shipping-consumer/src/telemetry.hpp @@ -0,0 +1,113 @@ +// telemetry.hpp — OpenTelemetry C++ traces over OTLP/HTTP. +// +// The exporters read OTEL_EXPORTER_OTLP_ENDPOINT (http://lgtm:4318 in compose; they append +// /v1/traces). The resource takes service.name from OTEL_SERVICE_NAME and +// OTEL_RESOURCE_ATTRIBUTES, with the name passed to init() as the default. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/sdk/trace/batch_span_processor_factory.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/sdk/trace/tracer_provider_factory.h" +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/span.h" + +namespace cndp::telemetry { + +namespace otel = opentelemetry; + +inline void init(const std::string& default_service_name) { + auto resource = otel::sdk::resource::Resource::Create({{"service.name", default_service_name}}); + resource = resource.Merge(otel::sdk::resource::OTELResourceDetector().Detect()); + + // Traces: batch processor -> OTLP/HTTP. + otel::exporter::otlp::OtlpHttpExporterOptions topts; + auto texporter = otel::exporter::otlp::OtlpHttpExporterFactory::Create(topts); + auto processor = otel::sdk::trace::BatchSpanProcessorFactory::Create( + std::move(texporter), otel::sdk::trace::BatchSpanProcessorOptions{}); + std::shared_ptr tp = + otel::sdk::trace::TracerProviderFactory::Create(std::move(processor), resource); + otel::trace::Provider::SetTracerProvider(tp); +} + +inline void shutdown() { + if (auto* tp = dynamic_cast( + otel::trace::Provider::GetTracerProvider().get())) { + tp->ForceFlush(); + tp->Shutdown(); + } +} + +using SpanPtr = otel::nostd::shared_ptr; + +inline otel::nostd::shared_ptr tracer(const std::string& name) { + return otel::trace::Provider::GetTracerProvider()->GetTracer(name); +} + +inline std::string trace_id_of(const SpanPtr& span) { + char buf[32]; + span->GetContext().trace_id().ToLowerBase16(buf); + return std::string(buf, 32); +} + +// W3C traceparent header value for a span (sampled). +inline std::string traceparent_of(const SpanPtr& span) { + char tid[32], sid[16]; + span->GetContext().trace_id().ToLowerBase16(tid); + span->GetContext().span_id().ToLowerBase16(sid); + return "00-" + std::string(tid, 32) + "-" + std::string(sid, 16) + "-01"; +} + +// Remote parent span context from a W3C traceparent header (00-<32 hex>-<16 hex>-<2 hex>). +inline bool parse_traceparent(const std::string& tp, otel::trace::SpanContext& out) { + auto nibble = [](char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; + }; + auto hex = [&](size_t off, uint8_t* dst, size_t n) { + for (size_t i = 0; i < n; ++i) { + int hi = nibble(tp[off + 2 * i]), lo = nibble(tp[off + 2 * i + 1]); + if (hi < 0 || lo < 0) return false; + dst[i] = static_cast(hi << 4 | lo); + } + return true; + }; + if (tp.size() < 55 || tp[2] != '-' || tp[35] != '-' || tp[52] != '-') return false; + uint8_t tid[16], sid[8]; + if (!hex(3, tid, 16) || !hex(36, sid, 8)) return false; + out = otel::trace::SpanContext( + otel::trace::TraceId(otel::nostd::span(tid)), + otel::trace::SpanId(otel::nostd::span(sid)), + otel::trace::TraceFlags(1), true); + return true; +} + +// Consumer span for one Kafka record, a child of the producer's span when the record carries a +// valid traceparent header and a new root otherwise. +inline SpanPtr start_consumer_span(const std::string& tracer_name, const std::string& span_name, + const std::string& traceparent, const std::string& order_id) { + otel::trace::StartSpanOptions so; + so.kind = otel::trace::SpanKind::kConsumer; + otel::trace::SpanContext parent = otel::trace::SpanContext::GetInvalid(); + if (parse_traceparent(traceparent, parent)) so.parent = parent; + return tracer(tracer_name)->StartSpan( + span_name, + {{"order.id", order_id}, {"messaging.system", "kafka"}, + {"messaging.destination.name", "order.placed"}}, + so); +} + +} // namespace cndp::telemetry diff --git a/examples/05-event-driven/dotnet/Containerfile.notification b/examples/05-event-driven/dotnet/Containerfile.notification index ffcb6d5e..cbdc4a9f 100644 --- a/examples/05-event-driven/dotnet/Containerfile.notification +++ b/examples/05-event-driven/dotnet/Containerfile.notification @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 NotificationConsumer/NotificationConsumer.csproj NotificationConsumer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore NotificationConsumer/NotificationConsumer.csproj COPY --chown=1001:0 NotificationConsumer/ NotificationConsumer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish NotificationConsumer/NotificationConsumer.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish NotificationConsumer/NotificationConsumer.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/05-event-driven/dotnet/Containerfile.order b/examples/05-event-driven/dotnet/Containerfile.order index 0f957cb9..817abc21 100644 --- a/examples/05-event-driven/dotnet/Containerfile.order +++ b/examples/05-event-driven/dotnet/Containerfile.order @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/05-event-driven/dotnet/Containerfile.shipping b/examples/05-event-driven/dotnet/Containerfile.shipping index 880cd7fe..694ce1df 100644 --- a/examples/05-event-driven/dotnet/Containerfile.shipping +++ b/examples/05-event-driven/dotnet/Containerfile.shipping @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 ShippingConsumer/ShippingConsumer.csproj ShippingConsumer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore ShippingConsumer/ShippingConsumer.csproj COPY --chown=1001:0 ShippingConsumer/ ShippingConsumer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish ShippingConsumer/ShippingConsumer.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish ShippingConsumer/ShippingConsumer.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/05-event-driven/dotnet/NotificationConsumer/Program.cs b/examples/05-event-driven/dotnet/NotificationConsumer/Program.cs index 708db9cc..8477f8e6 100644 --- a/examples/05-event-driven/dotnet/NotificationConsumer/Program.cs +++ b/examples/05-event-driven/dotnet/NotificationConsumer/Program.cs @@ -3,6 +3,7 @@ using Confluent.Kafka; using Npgsql; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -13,7 +14,13 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("notification-consumer")) - .AddOtlpExporter(o => o.Endpoint = new Uri(otelEndpoint)) + .AddSource("notification-consumer") + // 4318 is the OTLP/HTTP port: the exporter defaults to gRPC, and a code-set endpoint is used as given. + .AddOtlpExporter(o => + { + o.Endpoint = new Uri(otelEndpoint.TrimEnd('/') + "/v1/traces"); + o.Protocol = OtlpExportProtocol.HttpProtobuf; + }) .Build(); var tracer = tracerProvider!.GetTracer("notification-consumer"); diff --git a/examples/05-event-driven/dotnet/OrderService/Program.cs b/examples/05-event-driven/dotnet/OrderService/Program.cs index 281794f6..98625c22 100644 --- a/examples/05-event-driven/dotnet/OrderService/Program.cs +++ b/examples/05-event-driven/dotnet/OrderService/Program.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Confluent.Kafka; using Npgsql; +using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -11,8 +12,13 @@ .ConfigureResource(r => r.AddService("order-service")) .WithTracing(t => t .AddAspNetCoreInstrumentation() - .AddOtlpExporter(o => o.Endpoint = new Uri( - Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://lgtm:4318"))); + // 4318 is the OTLP/HTTP port: the exporter defaults to gRPC, and a code-set endpoint is used as given. + .AddOtlpExporter(o => + { + var endpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://lgtm:4318"; + o.Endpoint = new Uri(endpoint.TrimEnd('/') + "/v1/traces"); + o.Protocol = OtlpExportProtocol.HttpProtobuf; + })); var app = builder.Build(); diff --git a/examples/05-event-driven/dotnet/ShippingConsumer/Program.cs b/examples/05-event-driven/dotnet/ShippingConsumer/Program.cs index 532134af..d568d8a8 100644 --- a/examples/05-event-driven/dotnet/ShippingConsumer/Program.cs +++ b/examples/05-event-driven/dotnet/ShippingConsumer/Program.cs @@ -3,6 +3,7 @@ using Confluent.Kafka; using Npgsql; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -13,7 +14,13 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("shipping-consumer")) - .AddOtlpExporter(o => o.Endpoint = new Uri(otelEndpoint)) + .AddSource("shipping-consumer") + // 4318 is the OTLP/HTTP port: the exporter defaults to gRPC, and a code-set endpoint is used as given. + .AddOtlpExporter(o => + { + o.Endpoint = new Uri(otelEndpoint.TrimEnd('/') + "/v1/traces"); + o.Protocol = OtlpExportProtocol.HttpProtobuf; + }) .Build(); var tracer = tracerProvider!.GetTracer("shipping-consumer"); diff --git a/examples/05-event-driven/go/compose.yaml b/examples/05-event-driven/go/compose.yaml index 47f1a7a8..32e002bb 100644 --- a/examples/05-event-driven/go/compose.yaml +++ b/examples/05-event-driven/go/compose.yaml @@ -28,6 +28,7 @@ services: - DATABASE_URL=postgres://appuser:apppass@postgres:5432/appdb - KAFKA_BOOTSTRAP=kafka:9094 - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_SERVICE_NAME=order-service healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8080/healthz"] interval: 5s @@ -51,6 +52,7 @@ services: - DATABASE_URL=postgres://appuser:apppass@postgres:5432/appdb - KAFKA_BOOTSTRAP=kafka:9094 - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_SERVICE_NAME=shipping-consumer notification-consumer: build: @@ -68,3 +70,4 @@ services: - DATABASE_URL=postgres://appuser:apppass@postgres:5432/appdb - KAFKA_BOOTSTRAP=kafka:9094 - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_SERVICE_NAME=notification-consumer diff --git a/examples/05-event-driven/go/notification-consumer/cmd/consumer/main.go b/examples/05-event-driven/go/notification-consumer/cmd/consumer/main.go index dfef92c5..f0fe1425 100644 --- a/examples/05-event-driven/go/notification-consumer/cmd/consumer/main.go +++ b/examples/05-event-driven/go/notification-consumer/cmd/consumer/main.go @@ -10,12 +10,36 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/twmb/franz-go/pkg/kgo" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" ) func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer cancel() + res := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName(serviceName())) + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + if endpoint == "" { + endpoint = "http://lgtm:4318" + } + traceExp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(stripScheme(endpoint)), otlptracehttp.WithInsecure()) + if err != nil { + slog.Error("otlp exporter init failed", "err", err) + os.Exit(1) + } + tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExp), sdktrace.WithResource(res)) + defer tp.Shutdown(context.Background()) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + tracer := tp.Tracer("notification-consumer") + pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { slog.Error("db connect failed", "err", err) @@ -51,6 +75,11 @@ func main() { } fetches.EachRecord(func(rec *kgo.Record) { + parentCtx := otel.GetTextMapPropagator().Extract(ctx, &kafkaHeaderCarrier{headers: &rec.Headers}) + spanCtx, span := tracer.Start(parentCtx, "order.placed process", trace.WithSpanKind(trace.SpanKindConsumer)) + defer span.End() + span.SetAttributes(attribute.String("messaging.system", "kafka"), attribute.String("messaging.destination.name", "order.placed")) + var event struct { ID string `json:"id"` } @@ -59,7 +88,7 @@ func main() { return } - _, err := pool.Exec(ctx, + _, err := pool.Exec(spanCtx, "INSERT INTO notifications (order_id, channel) VALUES ($1, 'email') ON CONFLICT (order_id) DO NOTHING", event.ID) if err != nil { @@ -76,3 +105,44 @@ func main() { slog.Info("notification-consumer stopped") } + +type kafkaHeaderCarrier struct { + headers *[]kgo.RecordHeader +} + +func (c *kafkaHeaderCarrier) Get(key string) string { + for _, h := range *c.headers { + if h.Key == key { + return string(h.Value) + } + } + return "" +} + +func (c *kafkaHeaderCarrier) Set(key, val string) { + *c.headers = append(*c.headers, kgo.RecordHeader{Key: key, Value: []byte(val)}) +} + +func (c *kafkaHeaderCarrier) Keys() []string { + keys := make([]string, 0, len(*c.headers)) + for _, h := range *c.headers { + keys = append(keys, h.Key) + } + return keys +} + +func serviceName() string { + if n := os.Getenv("OTEL_SERVICE_NAME"); n != "" { + return n + } + return "notification-consumer" +} + +func stripScheme(url string) string { + for _, p := range []string{"http://", "https://"} { + if len(url) > len(p) && url[:len(p)] == p { + return url[len(p):] + } + } + return url +} diff --git a/examples/05-event-driven/go/notification-consumer/go.mod b/examples/05-event-driven/go/notification-consumer/go.mod index bd0be82c..c84cf032 100644 --- a/examples/05-event-driven/go/notification-consumer/go.mod +++ b/examples/05-event-driven/go/notification-consumer/go.mod @@ -5,15 +5,36 @@ go 1.27.2 require ( github.com/jackc/pgx/v5 v5.11.0 github.com/twmb/franz-go v1.22.1 + go.opentelemetry.io/otel v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 + go.opentelemetry.io/otel/sdk v1.47.0 + go.opentelemetry.io/otel/trace v1.47.0 ) require ( + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.3 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/pierrec/lz4/v4 v4.1.33 // indirect github.com/twmb/franz-go/pkg/kmsg v1.14.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.1 // indirect + golang.org/x/net v0.61.0 // indirect golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect golang.org/x/text v0.43.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect ) diff --git a/examples/05-event-driven/go/notification-consumer/go.sum b/examples/05-event-driven/go/notification-consumer/go.sum index bf21ad13..ff1b50f1 100644 --- a/examples/05-event-driven/go/notification-consumer/go.sum +++ b/examples/05-event-driven/go/notification-consumer/go.sum @@ -1,6 +1,21 @@ +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -13,22 +28,57 @@ github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLM github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/pierrec/lz4/v4 v4.1.33 h1:GjG1TJ1V4IzKP8L96muuuDNpTwd7D+l2ccXrjAbe014= github.com/pierrec/lz4/v4 v4.1.33/go.mod h1:7SE9MC2STkNtL4PIwGhjmyVwvILaGI9/COYQNBhKM/c= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/twmb/franz-go v1.22.1 h1:J7Xixbb7k0Itl39eaBot5PIblZh9IL3ZKYgo2yzlf40= github.com/twmb/franz-go v1.22.1/go.mod h1:b2qISbZgMTJRcIsltVqPz4+Bb2Lw/9bN+/Gd0C07kYw= github.com/twmb/franz-go/pkg/kmsg v1.14.0 h1:gSxrBEKWl3qnsx3QKWol5OEVujuPmIoDkhMt3didFKM= github.com/twmb/franz-go/pkg/kmsg v1.14.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw= +go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 h1:cFbkNigDInw+wVTXc+CyhUsoHGJCw0BR6mlNoRVfre0= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8/go.mod h1:QK6p7IgD7IZEZJBhhSUCib5X2vMOzlrGCGUw2XyVDh0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 h1:0SXBqli5dnuCKMjI/X3FM226zEaEa0GUcSCJtN1vL2o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/05-event-driven/go/order-service/cmd/server/main.go b/examples/05-event-driven/go/order-service/cmd/server/main.go index d942a8ae..04efc598 100644 --- a/examples/05-event-driven/go/order-service/cmd/server/main.go +++ b/examples/05-event-driven/go/order-service/cmd/server/main.go @@ -15,12 +15,38 @@ import ( "github.com/cenkalti/backoff/v7" "github.com/jackc/pgx/v5/pgxpool" "github.com/twmb/franz-go/pkg/kgo" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + res := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName(serviceName())) + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + if endpoint == "" { + endpoint = "http://lgtm:4318" + } + traceExp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(stripScheme(endpoint)), otlptracehttp.WithInsecure()) + if err != nil { + slog.Error("otlp exporter init failed", "err", err) + os.Exit(1) + } + tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExp), sdktrace.WithResource(res)) + defer tp.Shutdown(context.Background()) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + tracer := tp.Tracer("order-service") + pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { slog.Error("db connect failed", "err", err) @@ -73,18 +99,25 @@ func main() { event, _ := json.Marshal(map[string]any{ "id": id, "sku": in.SKU, "quantity": in.Quantity, "status": "pending", }) - rec := &kgo.Record{Topic: "order.placed", Key: []byte(id), Value: event} + produceCtx, span := tracer.Start(r.Context(), "order.placed publish", trace.WithSpanKind(trace.SpanKindProducer)) + span.SetAttributes(attribute.String("messaging.system", "kafka"), attribute.String("messaging.destination.name", "order.placed")) + headers := make([]kgo.RecordHeader, 0, 1) + otel.GetTextMapPropagator().Inject(produceCtx, &kafkaHeaderCarrier{headers: &headers}) + rec := &kgo.Record{Topic: "order.placed", Key: []byte(id), Value: event, Headers: headers} produceBackoff := backoff.NewExponentialBackOff() produceBackoff.InitialInterval = 500 * time.Millisecond produceBackoff.MaxInterval = 2 * time.Second - if _, err := backoff.Retry(r.Context(), func() (struct{}, error) { - return struct{}{}, producer.ProduceSync(r.Context(), rec).FirstErr() + if _, err := backoff.Retry(produceCtx, func() (struct{}, error) { + return struct{}{}, producer.ProduceSync(produceCtx, rec).FirstErr() }, backoff.WithBackOff(produceBackoff), backoff.WithMaxTries(5), backoff.WithNotify(func(err error, next time.Duration) { slog.Warn("kafka produce retry", "err", err, "next_in", next) })); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "produce failed") slog.Error("kafka produce failed", "err", err) } + span.End() slog.Info("order placed", "id", id, "sku", in.SKU) w.Header().Set("Content-Type", "application/json") @@ -119,7 +152,7 @@ func main() { json.NewEncoder(w).Encode(orders) }) - srv := &http.Server{Addr: ":8080", Handler: mux} + srv := &http.Server{Addr: ":8080", Handler: otelhttp.NewHandler(mux, "order-service")} go func() { sigCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stopSignals() @@ -141,3 +174,44 @@ func newUUID() string { b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } + +type kafkaHeaderCarrier struct { + headers *[]kgo.RecordHeader +} + +func (c *kafkaHeaderCarrier) Get(key string) string { + for _, h := range *c.headers { + if h.Key == key { + return string(h.Value) + } + } + return "" +} + +func (c *kafkaHeaderCarrier) Set(key, val string) { + *c.headers = append(*c.headers, kgo.RecordHeader{Key: key, Value: []byte(val)}) +} + +func (c *kafkaHeaderCarrier) Keys() []string { + keys := make([]string, 0, len(*c.headers)) + for _, h := range *c.headers { + keys = append(keys, h.Key) + } + return keys +} + +func serviceName() string { + if n := os.Getenv("OTEL_SERVICE_NAME"); n != "" { + return n + } + return "order-service" +} + +func stripScheme(url string) string { + for _, p := range []string{"http://", "https://"} { + if len(url) > len(p) && url[:len(p)] == p { + return url[len(p):] + } + } + return url +} diff --git a/examples/05-event-driven/go/order-service/go.mod b/examples/05-event-driven/go/order-service/go.mod index 6649672d..8af57ab8 100644 --- a/examples/05-event-driven/go/order-service/go.mod +++ b/examples/05-event-driven/go/order-service/go.mod @@ -6,15 +6,38 @@ require ( github.com/cenkalti/backoff/v7 v7.0.1 github.com/jackc/pgx/v5 v5.11.0 github.com/twmb/franz-go v1.22.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 + go.opentelemetry.io/otel v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 + go.opentelemetry.io/otel/sdk v1.47.0 + go.opentelemetry.io/otel/trace v1.47.0 ) require ( + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.3 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/pierrec/lz4/v4 v4.1.33 // indirect github.com/twmb/franz-go/pkg/kmsg v1.14.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.1 // indirect + golang.org/x/net v0.61.0 // indirect golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect golang.org/x/text v0.43.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect ) diff --git a/examples/05-event-driven/go/order-service/go.sum b/examples/05-event-driven/go/order-service/go.sum index 48dab6b2..fa4c3fd8 100644 --- a/examples/05-event-driven/go/order-service/go.sum +++ b/examples/05-event-driven/go/order-service/go.sum @@ -1,8 +1,25 @@ +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v7 v7.0.1 h1:+ulHti5s1d9QnfyOnLWVmQ/SaZ+TJQF9OdOI0i9BGZI= github.com/cenkalti/backoff/v7 v7.0.1/go.mod h1:qcKBGwsu4hpxHtQ8tWYsQ+ifzx2+sS+Xx/3jfe30lI8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -15,22 +32,59 @@ github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLM github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/pierrec/lz4/v4 v4.1.33 h1:GjG1TJ1V4IzKP8L96muuuDNpTwd7D+l2ccXrjAbe014= github.com/pierrec/lz4/v4 v4.1.33/go.mod h1:7SE9MC2STkNtL4PIwGhjmyVwvILaGI9/COYQNBhKM/c= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/twmb/franz-go v1.22.1 h1:J7Xixbb7k0Itl39eaBot5PIblZh9IL3ZKYgo2yzlf40= github.com/twmb/franz-go v1.22.1/go.mod h1:b2qISbZgMTJRcIsltVqPz4+Bb2Lw/9bN+/Gd0C07kYw= github.com/twmb/franz-go/pkg/kmsg v1.14.0 h1:gSxrBEKWl3qnsx3QKWol5OEVujuPmIoDkhMt3didFKM= github.com/twmb/franz-go/pkg/kmsg v1.14.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw= +go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 h1:cFbkNigDInw+wVTXc+CyhUsoHGJCw0BR6mlNoRVfre0= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8/go.mod h1:QK6p7IgD7IZEZJBhhSUCib5X2vMOzlrGCGUw2XyVDh0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 h1:0SXBqli5dnuCKMjI/X3FM226zEaEa0GUcSCJtN1vL2o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/05-event-driven/go/shipping-consumer/cmd/consumer/main.go b/examples/05-event-driven/go/shipping-consumer/cmd/consumer/main.go index f584380a..9910eae9 100644 --- a/examples/05-event-driven/go/shipping-consumer/cmd/consumer/main.go +++ b/examples/05-event-driven/go/shipping-consumer/cmd/consumer/main.go @@ -10,12 +10,36 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/twmb/franz-go/pkg/kgo" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" ) func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer cancel() + res := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName(serviceName())) + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + if endpoint == "" { + endpoint = "http://lgtm:4318" + } + traceExp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(stripScheme(endpoint)), otlptracehttp.WithInsecure()) + if err != nil { + slog.Error("otlp exporter init failed", "err", err) + os.Exit(1) + } + tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExp), sdktrace.WithResource(res)) + defer tp.Shutdown(context.Background()) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + tracer := tp.Tracer("shipping-consumer") + pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { slog.Error("db connect failed", "err", err) @@ -51,6 +75,11 @@ func main() { } fetches.EachRecord(func(rec *kgo.Record) { + parentCtx := otel.GetTextMapPropagator().Extract(ctx, &kafkaHeaderCarrier{headers: &rec.Headers}) + spanCtx, span := tracer.Start(parentCtx, "order.placed process", trace.WithSpanKind(trace.SpanKindConsumer)) + defer span.End() + span.SetAttributes(attribute.String("messaging.system", "kafka"), attribute.String("messaging.destination.name", "order.placed")) + var event struct { ID string `json:"id"` } @@ -59,7 +88,7 @@ func main() { return } - _, err := pool.Exec(ctx, + _, err := pool.Exec(spanCtx, "INSERT INTO shipments (order_id, status) VALUES ($1, 'scheduled') ON CONFLICT (order_id) DO NOTHING", event.ID) if err != nil { @@ -76,3 +105,44 @@ func main() { slog.Info("shipping-consumer stopped") } + +type kafkaHeaderCarrier struct { + headers *[]kgo.RecordHeader +} + +func (c *kafkaHeaderCarrier) Get(key string) string { + for _, h := range *c.headers { + if h.Key == key { + return string(h.Value) + } + } + return "" +} + +func (c *kafkaHeaderCarrier) Set(key, val string) { + *c.headers = append(*c.headers, kgo.RecordHeader{Key: key, Value: []byte(val)}) +} + +func (c *kafkaHeaderCarrier) Keys() []string { + keys := make([]string, 0, len(*c.headers)) + for _, h := range *c.headers { + keys = append(keys, h.Key) + } + return keys +} + +func serviceName() string { + if n := os.Getenv("OTEL_SERVICE_NAME"); n != "" { + return n + } + return "shipping-consumer" +} + +func stripScheme(url string) string { + for _, p := range []string{"http://", "https://"} { + if len(url) > len(p) && url[:len(p)] == p { + return url[len(p):] + } + } + return url +} diff --git a/examples/05-event-driven/go/shipping-consumer/go.mod b/examples/05-event-driven/go/shipping-consumer/go.mod index fd5770eb..568461c6 100644 --- a/examples/05-event-driven/go/shipping-consumer/go.mod +++ b/examples/05-event-driven/go/shipping-consumer/go.mod @@ -5,15 +5,36 @@ go 1.27.2 require ( github.com/jackc/pgx/v5 v5.11.0 github.com/twmb/franz-go v1.22.1 + go.opentelemetry.io/otel v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 + go.opentelemetry.io/otel/sdk v1.47.0 + go.opentelemetry.io/otel/trace v1.47.0 ) require ( + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.3 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/pierrec/lz4/v4 v4.1.33 // indirect github.com/twmb/franz-go/pkg/kmsg v1.14.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.1 // indirect + golang.org/x/net v0.61.0 // indirect golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect golang.org/x/text v0.43.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect ) diff --git a/examples/05-event-driven/go/shipping-consumer/go.sum b/examples/05-event-driven/go/shipping-consumer/go.sum index bf21ad13..ff1b50f1 100644 --- a/examples/05-event-driven/go/shipping-consumer/go.sum +++ b/examples/05-event-driven/go/shipping-consumer/go.sum @@ -1,6 +1,21 @@ +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -13,22 +28,57 @@ github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLM github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/pierrec/lz4/v4 v4.1.33 h1:GjG1TJ1V4IzKP8L96muuuDNpTwd7D+l2ccXrjAbe014= github.com/pierrec/lz4/v4 v4.1.33/go.mod h1:7SE9MC2STkNtL4PIwGhjmyVwvILaGI9/COYQNBhKM/c= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/twmb/franz-go v1.22.1 h1:J7Xixbb7k0Itl39eaBot5PIblZh9IL3ZKYgo2yzlf40= github.com/twmb/franz-go v1.22.1/go.mod h1:b2qISbZgMTJRcIsltVqPz4+Bb2Lw/9bN+/Gd0C07kYw= github.com/twmb/franz-go/pkg/kmsg v1.14.0 h1:gSxrBEKWl3qnsx3QKWol5OEVujuPmIoDkhMt3didFKM= github.com/twmb/franz-go/pkg/kmsg v1.14.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw= +go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 h1:cFbkNigDInw+wVTXc+CyhUsoHGJCw0BR6mlNoRVfre0= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8/go.mod h1:QK6p7IgD7IZEZJBhhSUCib5X2vMOzlrGCGUw2XyVDh0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 h1:0SXBqli5dnuCKMjI/X3FM226zEaEa0GUcSCJtN1vL2o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/05-event-driven/k8s/README.md b/examples/05-event-driven/k8s/README.md new file mode 100644 index 00000000..f317cdf2 --- /dev/null +++ b/examples/05-event-driven/k8s/README.md @@ -0,0 +1,78 @@ +# Example 05 on minikube + +The same three services and the same `verify.sh`, on a single-node Kubernetes cluster: +minikube with the Docker driver and containerd, Kubernetes v1.36.5. Appendix R in the +site walks through the design; this page is the command reference. + +## Layout + +| Path | Purpose | +|------|---------| +| `namespace.yaml` | Namespace `cndp-05` | +| `infra/` | Kafka (Strimzi `Kafka` and `KafkaNodePool`), Postgres 18 StatefulSet, Grafana LGTM, the Secret `cndp-postgres-app` and the schema ConfigMap | +| `base/` | The three Deployments and the order-service NodePort Service | +| `overlays//` | Image names and the environment variables that language reads | +| `setup-profile.sh` | Create or start the `cndp-k8s` profile with the NodePorts published on 127.0.0.1 | +| `build-images.sh` | `docker build` each image, then `minikube image load` it | +| `deploy.sh` | Strimzi operator, infrastructure, one language overlay | +| `verify-matrix.sh` | Build, deploy, verify and tear down each language in turn | +| `teardown.sh` | Remove the namespace, and with `--operator` the Strimzi release, CRDs and namespace | + +## Prerequisites + +Docker Engine with the `default` context, minikube v1.39.0, kubectl v1.36.5 and Helm 4 +(see [Prerequisites](../../PREREQUISITES.md)). The profile needs 8 GB of RAM, 4 CPUs and 40 GB of +disk. Run one workshop at a time: the scripts refuse to start while another minikube profile or +any other container is running, and they never stop them for you. + +## Run it + +```bash +cd examples/05-event-driven +k8s/setup-profile.sh # create the profile cndp-k8s +k8s/build-images.sh go # build and load the Go images +k8s/deploy.sh go # Strimzi, infrastructure, Go overlay +RUNTIME=kubernetes BASE=http://127.0.0.1:30080 ./verify.sh +k8s/teardown.sh # remove the cndp-05 namespace +``` + +For all six languages in sequence: `OUT=/tmp/cndp-k8s-matrix k8s/verify-matrix.sh`. + +The scripts always name the profile (`--context cndp-k8s`, `--kube-context cndp-k8s`) and start +minikube with `--keep-context`. minikube's restart path overrides that flag when the API-server +host port changes, so `setup-profile.sh` also records the current-context before starting and +restores it afterwards. Use the same flags in your own commands, and check +`kubectl config current-context` after any `minikube start` you run yourself. + +## Reach it from the host + +Two NodePorts are published on 127.0.0.1 when the profile is created. There is no port-forward, +tunnel or `minikube service`. + +| Port | Service | +|------|---------| +| 127.0.0.1:30080 | order-service | +| 127.0.0.1:30300 | Grafana (anonymous admin, as in compose) | + +Published ports are fixed at creation. Changing them means recreating the profile. + +## Clean up + +```bash +k8s/teardown.sh --operator # namespace, Strimzi release, CRDs, strimzi namespace +minikube stop -p cndp-k8s # free the memory; the profile stays +minikube delete -p cndp-k8s # optional: remove the profile +``` + +## Render without a cluster + +`infra/` reads the schema from `../db/init`, outside its directory, so it needs the load +restrictor lifted: + +```bash +kubectl kustomize --load-restrictor LoadRestrictionsNone k8s/infra +kubectl kustomize k8s/overlays/go +``` + +CI renders `infra/` and every overlay and checks them with kubeconform against the Kubernetes +1.36.5 schemas. diff --git a/examples/05-event-driven/k8s/base/deployments.yaml b/examples/05-event-driven/k8s/base/deployments.yaml new file mode 100644 index 00000000..1ac6f0d1 --- /dev/null +++ b/examples/05-event-driven/k8s/base/deployments.yaml @@ -0,0 +1,161 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: order-service + labels: + app.kubernetes.io/name: order-service +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: order-service + template: + metadata: + labels: + app.kubernetes.io/name: order-service + spec: + containers: + - name: order-service + image: order-service + imagePullPolicy: Never + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + env: + - name: OTEL_SERVICE_NAME + value: order-service + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://lgtm:4318 + - name: PG_USER + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: username + - name: PG_PASSWORD + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: password + ports: + - name: http + containerPort: 8080 + startupProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + timeoutSeconds: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 768Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shipping-consumer + labels: + app.kubernetes.io/name: shipping-consumer +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: shipping-consumer + template: + metadata: + labels: + app.kubernetes.io/name: shipping-consumer + spec: + containers: + - name: shipping-consumer + image: shipping-consumer + imagePullPolicy: Never + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + env: + - name: OTEL_SERVICE_NAME + value: shipping-consumer + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://lgtm:4318 + - name: PG_USER + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: username + - name: PG_PASSWORD + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: password + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 768Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notification-consumer + labels: + app.kubernetes.io/name: notification-consumer +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: notification-consumer + template: + metadata: + labels: + app.kubernetes.io/name: notification-consumer + spec: + containers: + - name: notification-consumer + image: notification-consumer + imagePullPolicy: Never + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + env: + - name: OTEL_SERVICE_NAME + value: notification-consumer + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://lgtm:4318 + - name: PG_USER + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: username + - name: PG_PASSWORD + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: password + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 768Mi diff --git a/examples/05-event-driven/k8s/base/kustomization.yaml b/examples/05-event-driven/k8s/base/kustomization.yaml new file mode 100644 index 00000000..cde58690 --- /dev/null +++ b/examples/05-event-driven/k8s/base/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +labels: + - pairs: + app.kubernetes.io/part-of: cndp-05 + includeSelectors: false +resources: + - deployments.yaml + - service.yaml diff --git a/examples/05-event-driven/k8s/base/service.yaml b/examples/05-event-driven/k8s/base/service.yaml new file mode 100644 index 00000000..8cce3794 --- /dev/null +++ b/examples/05-event-driven/k8s/base/service.yaml @@ -0,0 +1,16 @@ +# order-service is published on node port 30080 (minikube maps it to 127.0.0.1:30080). +apiVersion: v1 +kind: Service +metadata: + name: order-service + labels: + app.kubernetes.io/name: order-service +spec: + type: NodePort + selector: + app.kubernetes.io/name: order-service + ports: + - name: http + port: 8080 + targetPort: http + nodePort: 30080 diff --git a/examples/05-event-driven/k8s/build-images.sh b/examples/05-event-driven/k8s/build-images.sh new file mode 100755 index 00000000..a4709a82 --- /dev/null +++ b/examples/05-event-driven/k8s/build-images.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Build the Example 05 images with docker (host BuildKit caches apply) and load +# them into the minikube node's containerd. Names: localhost/cndp-05/-:v1. +# +# k8s/build-images.sh go # one language +# k8s/build-images.sh go python # several +set -euo pipefail +# shellcheck source=lib.sh +# shellcheck source-path=SCRIPTDIR +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +[[ $# -ge 1 ]] || fail "usage: build-images.sh ... (${LANGS[*]})" +require_profile_running + +SERVICES=(order-service shipping-consumer notification-consumer) + +# build_args prints " " relative to the example directory. +# .NET builds from one context with a Containerfile per service; the others have a +# directory per service. +build_args() { + local lang="$1" svc="$2" short + if [[ "$lang" == dotnet ]]; then + short="${svc%%-*}" + echo "dotnet Containerfile.${short}" + else + echo "${lang}/${svc} Containerfile" + fi +} + +for lang in "$@"; do + valid_lang "$lang" || fail "unknown language '$lang' (${LANGS[*]})" + for svc in "${SERVICES[@]}"; do + read -r ctx cf <<<"$(build_args "$lang" "$svc")" + image="localhost/cndp-05/${lang}-${svc}:${IMAGE_TAG}" + log "build $image" + docker build -t "$image" -f "$EXAMPLE_DIR/$ctx/$cf" "$EXAMPLE_DIR/$ctx" + log "load $image into $PROFILE" + minikube -p "$PROFILE" image load --overwrite=true "$image" + done +done +log "images in the node:" +minikube -p "$PROFILE" image ls --format table | grep -E "cndp-05" || true diff --git a/examples/05-event-driven/k8s/deploy.sh b/examples/05-event-driven/k8s/deploy.sh new file mode 100755 index 00000000..550f5dd3 --- /dev/null +++ b/examples/05-event-driven/k8s/deploy.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Deploy Example 05 for one language into the cndp-05 namespace. +# +# Order: Strimzi operator (Helm, once), namespace, infrastructure (Kafka, Postgres, +# LGTM) and wait for it, then the language overlay. The images must already be +# loaded: k8s/build-images.sh . +# +# k8s/deploy.sh go +set -euo pipefail +# shellcheck source=lib.sh +# shellcheck source-path=SCRIPTDIR +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +[[ $# -eq 1 ]] || fail "usage: deploy.sh (${LANGS[*]})" +lang="$1" +valid_lang "$lang" || fail "unknown language '$lang' (${LANGS[*]})" +require_profile_running + +log "Strimzi operator $STRIMZI_VERSION in namespace $STRIMZI_NAMESPACE" +hm upgrade --install strimzi "$STRIMZI_CHART" \ + --version "$STRIMZI_VERSION" \ + --namespace "$STRIMZI_NAMESPACE" --create-namespace \ + --set watchAnyNamespace=true \ + --wait --timeout 5m + +log "namespace $NAMESPACE and infrastructure" +kc apply -f "$K8S_DIR/namespace.yaml" +render "$K8S_DIR/infra" | kc apply -f - + +log "waiting for Kafka (up to 600 s)" +kcn wait kafka/cndp --for=condition=Ready --timeout=600s +log "waiting for Postgres and LGTM" +kcn rollout status statefulset/cndp-postgres --timeout=300s +kcn rollout status deployment/lgtm --timeout=300s + +log "overlay $lang" +render "$K8S_DIR/overlays/$lang" | kc apply -f - +for d in shipping-consumer notification-consumer order-service; do + kcn rollout status "deployment/$d" --timeout=420s +done + +log "polling http://127.0.0.1:30080/healthz" +for _ in $(seq 1 60); do + if curl -sf http://127.0.0.1:30080/healthz | grep -q '"status":"ok"'; then + log "order-service is healthy on 127.0.0.1:30080" + exit 0 + fi + sleep 2 +done +kcn get pods -o wide >&2 +fail "order-service did not answer /healthz on 127.0.0.1:30080" diff --git a/examples/05-event-driven/k8s/infra/kafka.yaml b/examples/05-event-driven/k8s/infra/kafka.yaml new file mode 100644 index 00000000..cc7e3253 --- /dev/null +++ b/examples/05-event-driven/k8s/infra/kafka.yaml @@ -0,0 +1,48 @@ +# Single-node KRaft cluster managed by the Strimzi operator (namespace strimzi). +# The plain listener on 9092 is reachable in-cluster as cndp-kafka-bootstrap:9092. +apiVersion: kafka.strimzi.io/v1 +kind: KafkaNodePool +metadata: + name: dual-role + labels: + strimzi.io/cluster: cndp +spec: + replicas: 1 + roles: + - controller + - broker + storage: + type: jbod + volumes: + - id: 0 + type: persistent-claim + size: 2Gi + deleteClaim: true + resources: + requests: + cpu: 250m + memory: 1Gi + limits: + memory: 2Gi +--- +apiVersion: kafka.strimzi.io/v1 +kind: Kafka +metadata: + name: cndp +spec: + kafka: + version: 4.3.1 + listeners: + - name: plain + port: 9092 + type: internal + tls: false + config: + auto.create.topics.enable: true + num.partitions: 3 + default.replication.factor: 1 + min.insync.replicas: 1 + offsets.topic.replication.factor: 1 + transaction.state.log.replication.factor: 1 + transaction.state.log.min.isr: 1 + group.initial.rebalance.delay.ms: 0 diff --git a/examples/05-event-driven/k8s/infra/kustomization.yaml b/examples/05-event-driven/k8s/infra/kustomization.yaml new file mode 100644 index 00000000..73c20fd5 --- /dev/null +++ b/examples/05-event-driven/k8s/infra/kustomization.yaml @@ -0,0 +1,28 @@ +# Shared infrastructure for Example 05: Kafka (Strimzi), Postgres and the LGTM stack. +# Render with the schema from ../../db/init, which sits outside this directory: +# kubectl kustomize --load-restrictor LoadRestrictionsNone infra | kubectl apply -f - +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +labels: + - pairs: + app.kubernetes.io/part-of: cndp-05 + includeSelectors: false +resources: + - kafka.yaml + - postgres.yaml + - lgtm.yaml +secretGenerator: + - name: cndp-postgres-app + literals: + - username=appuser + - password=apppass + options: + disableNameSuffixHash: true +configMapGenerator: + - name: postgres-init + files: + - ../../db/init/00-extensions.sql + - ../../db/init/01-schema.sql + options: + disableNameSuffixHash: true diff --git a/examples/05-event-driven/k8s/infra/lgtm.yaml b/examples/05-event-driven/k8s/infra/lgtm.yaml new file mode 100644 index 00000000..a192e263 --- /dev/null +++ b/examples/05-event-driven/k8s/infra/lgtm.yaml @@ -0,0 +1,70 @@ +# Grafana LGTM (Loki, Grafana, Tempo, Mimir) with an OpenTelemetry Collector in one image. +# lgtm:4318 receives OTLP/HTTP; Grafana is published on node port 30300. +apiVersion: v1 +kind: Service +metadata: + name: lgtm +spec: + selector: + app.kubernetes.io/name: lgtm + ports: + - name: otlp-http + port: 4318 + targetPort: otlp-http +--- +apiVersion: v1 +kind: Service +metadata: + name: lgtm-grafana +spec: + type: NodePort + selector: + app.kubernetes.io/name: lgtm + ports: + - name: grafana + port: 3000 + targetPort: grafana + nodePort: 30300 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lgtm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: lgtm + template: + metadata: + labels: + app.kubernetes.io/name: lgtm + spec: + containers: + - name: lgtm + image: docker.io/grafana/otel-lgtm:0.36.0 + env: + - name: GF_AUTH_ANONYMOUS_ENABLED + value: "true" + - name: GF_AUTH_ANONYMOUS_ORG_ROLE + value: Admin + - name: GF_AUTH_DISABLE_LOGIN_FORM + value: "true" + ports: + - name: grafana + containerPort: 3000 + - name: otlp-http + containerPort: 4318 + readinessProbe: + httpGet: + path: /api/health + port: grafana + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 30 + resources: + requests: + cpu: 250m + memory: 768Mi + limits: + memory: 1536Mi diff --git a/examples/05-event-driven/k8s/infra/postgres.yaml b/examples/05-event-driven/k8s/infra/postgres.yaml new file mode 100644 index 00000000..20d8d16b --- /dev/null +++ b/examples/05-event-driven/k8s/infra/postgres.yaml @@ -0,0 +1,84 @@ +# Postgres 18 with the Example 05 schema. Same demo credentials as compose. +apiVersion: v1 +kind: Service +metadata: + name: cndp-postgres-rw +spec: + selector: + app.kubernetes.io/name: cndp-postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: cndp-postgres +spec: + serviceName: cndp-postgres-rw + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: cndp-postgres + template: + metadata: + labels: + app.kubernetes.io/name: cndp-postgres + spec: + containers: + - name: postgres + image: docker.io/library/postgres:18.6-alpine + args: + - postgres + - -c + - log_statement=all + - -c + - shared_buffers=128MB + env: + - name: POSTGRES_DB + value: appdb + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: cndp-postgres-app + key: password + - name: POSTGRES_INITDB_ARGS + value: --encoding=UTF8 --locale=C + ports: + - name: postgres + containerPort: 5432 + readinessProbe: + exec: + command: ["pg_isready", "-U", "appuser", "-d", "appdb"] + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + volumeMounts: + - name: data + mountPath: /var/lib/postgresql + - name: init + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init + configMap: + name: postgres-init + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/examples/05-event-driven/k8s/lib.sh b/examples/05-event-driven/k8s/lib.sh new file mode 100755 index 00000000..fb2fb4e6 --- /dev/null +++ b/examples/05-event-driven/k8s/lib.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Shared settings and helpers for the Example 05 minikube scripts. Source it; do not run it. +# +# Every kubectl and helm call names the profile explicitly. Only setup-profile.sh +# touches kubectl's current-context, to restore the value it recorded before +# `minikube start`; nothing runs `minikube config set`. + +# shellcheck disable=SC2034 # variables are used by the scripts that source this file +PROFILE="${PROFILE:-cndp-k8s}" +NAMESPACE="${NAMESPACE:-cndp-05}" +STRIMZI_NAMESPACE="${STRIMZI_NAMESPACE:-strimzi}" +STRIMZI_VERSION="${STRIMZI_VERSION:-1.2.0}" +STRIMZI_CHART="oci://quay.io/strimzi-helm/strimzi-kafka-operator" +IMAGE_TAG="v1" +LANGS=(spring-boot quarkus dotnet python cpp go) + +K8S_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLE_DIR="$(dirname "$K8S_DIR")" + +fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } +log() { printf '==> %s\n' "$*"; } + +kc() { kubectl --context "$PROFILE" "$@"; } +kcn() { kubectl --context "$PROFILE" -n "$NAMESPACE" "$@"; } +hm() { helm --kube-context "$PROFILE" "$@"; } + +# Render a kustomization. infra/ reads the schema from ../../db/init, which is +# outside its directory, so the load restrictor is lifted for rendering. +render() { kubectl kustomize --load-restrictor LoadRestrictionsNone "$1"; } + +valid_lang() { + local l + for l in "${LANGS[@]}"; do [[ "$l" == "$1" ]] && return 0; done + return 1 +} + +# Every profile except $PROFILE that is Running, one per line. +other_running_profiles() { + minikube profile list -o json 2>/dev/null | python3 -c ' +import json, sys +try: + data = json.load(sys.stdin) or {} +except Exception: + data = {} +for p in data.get("valid") or []: + if p.get("Name") != sys.argv[1] and p.get("Status") == "Running": + print(p["Name"]) +' "$PROFILE" +} + +require_profile_running() { + minikube status -p "$PROFILE" >/dev/null 2>&1 \ + || fail "profile $PROFILE is not running. Run k8s/setup-profile.sh first." +} diff --git a/examples/05-event-driven/k8s/namespace.yaml b/examples/05-event-driven/k8s/namespace.yaml new file mode 100644 index 00000000..60db2cfd --- /dev/null +++ b/examples/05-event-driven/k8s/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cndp-05 + labels: + app.kubernetes.io/part-of: cndp-05 diff --git a/examples/05-event-driven/k8s/overlays/cpp/kustomization.yaml b/examples/05-event-driven/k8s/overlays/cpp/kustomization.yaml new file mode 100644 index 00000000..41b10329 --- /dev/null +++ b/examples/05-event-driven/k8s/overlays/cpp/kustomization.yaml @@ -0,0 +1,35 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +resources: + - ../../base +images: + - name: order-service + newName: localhost/cndp-05/cpp-order-service + newTag: v1 + - name: shipping-consumer + newName: localhost/cndp-05/cpp-shipping-consumer + newTag: v1 + - name: notification-consumer + newName: localhost/cndp-05/cpp-notification-consumer + newTag: v1 +patches: + - target: + kind: Deployment + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: PG_CONNINFO + value: "postgresql://$(PG_USER):$(PG_PASSWORD)@cndp-postgres-rw:5432/appdb" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" + - op: replace + path: /spec/template/spec/containers/0/resources/requests/memory + value: 128Mi + - op: replace + path: /spec/template/spec/containers/0/resources/limits/memory + value: 256Mi diff --git a/examples/05-event-driven/k8s/overlays/dotnet/kustomization.yaml b/examples/05-event-driven/k8s/overlays/dotnet/kustomization.yaml new file mode 100644 index 00000000..e331ba22 --- /dev/null +++ b/examples/05-event-driven/k8s/overlays/dotnet/kustomization.yaml @@ -0,0 +1,39 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +resources: + - ../../base +images: + - name: order-service + newName: localhost/cndp-05/dotnet-order-service + newTag: v1 + - name: shipping-consumer + newName: localhost/cndp-05/dotnet-shipping-consumer + newTag: v1 + - name: notification-consumer + newName: localhost/cndp-05/dotnet-notification-consumer + newTag: v1 +patches: + - target: + kind: Deployment + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: DATABASE_URL + value: "Host=cndp-postgres-rw;Port=5432;Database=appdb;Username=$(PG_USER);Password=$(PG_PASSWORD)" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" + # compose sets SERVICE_VERSION on the order-service only + - target: + kind: Deployment + name: order-service + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SERVICE_VERSION + value: "1.0.0" diff --git a/examples/05-event-driven/k8s/overlays/go/kustomization.yaml b/examples/05-event-driven/k8s/overlays/go/kustomization.yaml new file mode 100644 index 00000000..d1b44546 --- /dev/null +++ b/examples/05-event-driven/k8s/overlays/go/kustomization.yaml @@ -0,0 +1,35 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +resources: + - ../../base +images: + - name: order-service + newName: localhost/cndp-05/go-order-service + newTag: v1 + - name: shipping-consumer + newName: localhost/cndp-05/go-shipping-consumer + newTag: v1 + - name: notification-consumer + newName: localhost/cndp-05/go-notification-consumer + newTag: v1 +patches: + - target: + kind: Deployment + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: DATABASE_URL + value: "postgres://$(PG_USER):$(PG_PASSWORD)@cndp-postgres-rw:5432/appdb" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" + - op: replace + path: /spec/template/spec/containers/0/resources/requests/memory + value: 128Mi + - op: replace + path: /spec/template/spec/containers/0/resources/limits/memory + value: 256Mi diff --git a/examples/05-event-driven/k8s/overlays/python/kustomization.yaml b/examples/05-event-driven/k8s/overlays/python/kustomization.yaml new file mode 100644 index 00000000..eda143f0 --- /dev/null +++ b/examples/05-event-driven/k8s/overlays/python/kustomization.yaml @@ -0,0 +1,35 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +resources: + - ../../base +images: + - name: order-service + newName: localhost/cndp-05/python-order-service + newTag: v1 + - name: shipping-consumer + newName: localhost/cndp-05/python-shipping-consumer + newTag: v1 + - name: notification-consumer + newName: localhost/cndp-05/python-notification-consumer + newTag: v1 +patches: + - target: + kind: Deployment + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: DATABASE_URL + value: "postgres://$(PG_USER):$(PG_PASSWORD)@cndp-postgres-rw:5432/appdb" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: KAFKA_BOOTSTRAP + value: "cndp-kafka-bootstrap:9092" + - op: replace + path: /spec/template/spec/containers/0/resources/requests/memory + value: 128Mi + - op: replace + path: /spec/template/spec/containers/0/resources/limits/memory + value: 256Mi diff --git a/examples/05-event-driven/k8s/overlays/quarkus/kustomization.yaml b/examples/05-event-driven/k8s/overlays/quarkus/kustomization.yaml new file mode 100644 index 00000000..bb94103f --- /dev/null +++ b/examples/05-event-driven/k8s/overlays/quarkus/kustomization.yaml @@ -0,0 +1,39 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +resources: + - ../../base +images: + - name: order-service + newName: localhost/cndp-05/quarkus-order-service + newTag: v1 + - name: shipping-consumer + newName: localhost/cndp-05/quarkus-shipping-consumer + newTag: v1 + - name: notification-consumer + newName: localhost/cndp-05/quarkus-notification-consumer + newTag: v1 +patches: + - target: + kind: Deployment + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: QUARKUS_DATASOURCE_JDBC_URL + value: "jdbc:postgresql://cndp-postgres-rw:5432/appdb" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: QUARKUS_DATASOURCE_USERNAME + value: "$(PG_USER)" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: QUARKUS_DATASOURCE_PASSWORD + value: "$(PG_PASSWORD)" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: KAFKA_BOOTSTRAP_SERVERS + value: "cndp-kafka-bootstrap:9092" diff --git a/examples/05-event-driven/k8s/overlays/spring-boot/kustomization.yaml b/examples/05-event-driven/k8s/overlays/spring-boot/kustomization.yaml new file mode 100644 index 00000000..034a2de2 --- /dev/null +++ b/examples/05-event-driven/k8s/overlays/spring-boot/kustomization.yaml @@ -0,0 +1,39 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cndp-05 +resources: + - ../../base +images: + - name: order-service + newName: localhost/cndp-05/spring-boot-order-service + newTag: v1 + - name: shipping-consumer + newName: localhost/cndp-05/spring-boot-shipping-consumer + newTag: v1 + - name: notification-consumer + newName: localhost/cndp-05/spring-boot-notification-consumer + newTag: v1 +patches: + - target: + kind: Deployment + patch: |- + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SPRING_DATASOURCE_URL + value: "jdbc:postgresql://cndp-postgres-rw:5432/appdb" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SPRING_DATASOURCE_USERNAME + value: "$(PG_USER)" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SPRING_DATASOURCE_PASSWORD + value: "$(PG_PASSWORD)" + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: "cndp-kafka-bootstrap:9092" diff --git a/examples/05-event-driven/k8s/setup-profile.sh b/examples/05-event-driven/k8s/setup-profile.sh new file mode 100755 index 00000000..3976d6fa --- /dev/null +++ b/examples/05-event-driven/k8s/setup-profile.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Create or start the minikube profile for Example 05. +# +# Docker driver, containerd runtime, Kubernetes v1.36.5. The order-service and +# Grafana NodePorts are published on 127.0.0.1 when the profile is created, so +# the host reaches them without port-forward, tunnels or `minikube service`. +# Published ports are fixed at creation: changing them means recreating the profile. +# +# The script refuses to run while another minikube profile is Running or while +# containers other than this profile's node are running. It never stops anything +# and never deletes a profile. Every minikube start passes --keep-context, and the +# script also restores kubectl's prior current-context afterwards, because +# minikube's restart path overrides the flag (see the restore block below). +# +# k8s/setup-profile.sh +set -euo pipefail +# shellcheck source=lib.sh +# shellcheck source-path=SCRIPTDIR +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +MEMORY="${MINIKUBE_MEMORY:-8g}" +CPUS="${MINIKUBE_CPUS:-4}" +DISK="${MINIKUBE_DISK:-40g}" +K8S_VERSION="${KUBERNETES_VERSION:-v1.36.5}" +MINIKUBE_MIN="v1.39.0" +NODE_PORTS=(30080 30300) # order-service, Grafana +PORTS_ARG="" +for p in "${NODE_PORTS[@]}"; do PORTS_ARG+="${PORTS_ARG:+,}127.0.0.1:${p}:${p}"; done + +for tool in docker minikube kubectl helm python3 ss; do + command -v "$tool" >/dev/null 2>&1 || fail "$tool is not in PATH (see the prerequisites page)" +done + +# --- Docker Engine --- +ctx="$(docker context show 2>/dev/null || true)" +[[ "$ctx" == "default" ]] || fail "docker context is '${ctx:-unknown}', expected 'default'. Fix: docker context use default" +docker info >/dev/null 2>&1 || fail "cannot reach the Docker daemon. Fix: sudo systemctl enable --now docker" +log "Docker Engine reachable (context default)" + +# --- minikube floor --- +mk_ver="$(minikube version --short 2>/dev/null || echo v0.0.0)" +[[ "$(printf '%s\n%s\n' "$MINIKUBE_MIN" "$mk_ver" | sort -V | head -1)" == "$MINIKUBE_MIN" ]] \ + || fail "minikube $mk_ver is older than $MINIKUBE_MIN" + +# --- kernel limits --- +inotify="$(sysctl -n fs.inotify.max_user_instances 2>/dev/null || echo 0)" +if (( inotify < 256 )); then + printf 'fs.inotify.max_user_instances is %d (need 256 or more). Fix:\n' "$inotify" >&2 + printf ' printf "fs.inotify.max_user_instances = 512\\nfs.inotify.max_user_watches = 524288\\n" | sudo tee /etc/sysctl.d/99-kubernetes.conf\n' >&2 + printf ' sudo sysctl -p /etc/sysctl.d/99-kubernetes.conf\n' >&2 + exit 1 +fi + +# --- one workshop at a time --- +others="$(other_running_profiles || true)" +if [[ -n "$others" ]]; then + printf 'Other minikube profiles are running:\n' >&2 + while read -r n; do printf ' - %s\n' "$n" >&2; done <<<"$others" + fail "stop them yourself (minikube stop -p ) and re-run; this script never stops another profile" +fi +foreign="$(docker ps --format '{{.Names}}' | grep -vx "$PROFILE" || true)" +if [[ -n "$foreign" ]]; then + printf 'Containers are running:\n' >&2 + while read -r n; do printf ' - %s\n' "$n" >&2; done <<<"$foreign" + fail "stop them yourself (for example docker compose down) and re-run" +fi + +# Record kubectl's current-context (possibly empty) before any `minikube start`. +prev_context="$(kubectl config current-context 2>/dev/null || true)" +# set-context (not use-context) also restores a prior context whose entry no longer exists. +# With several KUBECONFIG files kubectl writes current-context into the first one; the +# merged value is what matters. +restore_context() { + if [[ -n "$prev_context" ]]; then + kubectl config set current-context "$prev_context" >/dev/null + else + kubectl config unset current-context >/dev/null + fi +} +# Restore on every exit, including a failed start or Ctrl-C. +trap restore_context EXIT + +profile_json="$(minikube profile list -o json 2>/dev/null || echo '{}')" +exists="$(python3 -c ' +import json, sys +try: + data = json.loads(sys.argv[2]) or {} +except Exception: + data = {} +print("yes" if any(p.get("Name") == sys.argv[1] for p in (data.get("valid") or []) + (data.get("invalid") or [])) else "no") +' "$PROFILE" "$profile_json")" + +running=no +if [[ "$exists" == "yes" ]] && minikube status -p "$PROFILE" >/dev/null 2>&1; then running=yes; fi + +if [[ "$running" == "no" ]]; then + for p in "${NODE_PORTS[@]}"; do + if [[ -n "$(ss -ltnH "sport = :$p" 2>/dev/null)" ]]; then + fail "host port $p is in use; free it and re-run" + fi + done +fi + +if [[ "$exists" == "yes" ]]; then + # An existing profile must match what this example needs; settings are fixed at creation. + mismatch="$(python3 -c ' +import json, sys +data = json.loads(sys.argv[2]) or {} +want = {"Driver": "docker", "ContainerRuntime": "containerd", "KubernetesVersion": sys.argv[3]} +for p in (data.get("valid") or []) + (data.get("invalid") or []): + if p.get("Name") != sys.argv[1]: + continue + cfg = p.get("Config") or {} + kcfg = cfg.get("KubernetesConfig") or {} + have = {"Driver": cfg.get("Driver"), "ContainerRuntime": kcfg.get("ContainerRuntime"), "KubernetesVersion": kcfg.get("KubernetesVersion")} + for k, v in want.items(): + if have[k] != v: + print("%s is %s, expected %s" % (k, have[k] or "unknown", v)) +' "$PROFILE" "$profile_json" "$K8S_VERSION")" + [[ -z "$mismatch" ]] || fail "profile $PROFILE does not match this example: ${mismatch//$'\n'/; }. Settings are fixed at creation; recreate the profile." + if [[ "$running" == "yes" ]]; then + log "profile $PROFILE is already running" + else + log "starting profile $PROFILE (published ports persist)" + minikube start -p "$PROFILE" --keep-context + fi +else + log "creating $PROFILE (Kubernetes $K8S_VERSION, $MEMORY RAM, $CPUS CPUs, $DISK disk, docker driver, containerd)" + log "publishing NodePorts: $PORTS_ARG" + minikube start -p "$PROFILE" \ + --driver=docker \ + --container-runtime=containerd \ + --kubernetes-version="$K8S_VERSION" \ + --memory="$MEMORY" --cpus="$CPUS" --disk-size="$DISK" \ + --ports="$PORTS_ARG" \ + --keep-context +fi + +# The single sanctioned current-context restore. --keep-context is honoured when a +# profile is created, but when a stopped docker-driver profile restarts and its +# API-server host port changed, minikube v1.39.0 calls kubeconfig.UpdateEndpoint with +# KeepContext false and sets current-context to the profile anyway. Put back whatever +# was recorded before the start, then assert it. +restore_context +now_context="$(kubectl config current-context 2>/dev/null || true)" +[[ "$now_context" == "$prev_context" ]] \ + || fail "kubectl current-context is '${now_context:-unset}', expected '${prev_context:-unset}'" + +# --- published ports must be loopback only --- +for p in "${NODE_PORTS[@]}"; do + published="$(docker port "$PROFILE" "${p}/tcp" 2>/dev/null || true)" + if ! grep -qF "127.0.0.1:${p}" <<<"$published" || grep -qF "0.0.0.0" <<<"$published"; then + fail "profile $PROFILE does not publish $p on 127.0.0.1 only (docker port: ${published:-none}). Ports are fixed at creation; recreate the profile." + fi +done +log "NodePorts ${NODE_PORTS[*]} are published on 127.0.0.1 only" + +log "cluster health (context $PROFILE)" +kc wait --for=condition=Ready node --all --timeout=180s +kc get nodes +log "profile $PROFILE is ready; kubectl current-context is ${prev_context:-unset}, as before" diff --git a/examples/05-event-driven/k8s/teardown.sh b/examples/05-event-driven/k8s/teardown.sh new file mode 100755 index 00000000..45d8b9c0 --- /dev/null +++ b/examples/05-event-driven/k8s/teardown.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Remove Example 05 from the cluster. The profile itself is never deleted. +# +# k8s/teardown.sh # delete the cndp-05 namespace (Kafka, Postgres, LGTM, apps, volumes) +# k8s/teardown.sh --operator # also remove the Strimzi release, its CRDs and namespace +set -euo pipefail +# shellcheck source=lib.sh +# shellcheck source-path=SCRIPTDIR +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +operator=0 +case "${1:-}" in + "") ;; + --operator) operator=1 ;; + *) fail "usage: teardown.sh [--operator]" ;; +esac +minikube status -p "$PROFILE" >/dev/null 2>&1 || fail "profile $PROFILE is not running; nothing to tear down" + +log "deleting namespace $NAMESPACE" +kc delete namespace "$NAMESPACE" --ignore-not-found --wait=true --timeout=300s + +if (( operator )); then + if hm status strimzi -n "$STRIMZI_NAMESPACE" >/dev/null 2>&1; then + log "uninstalling the Strimzi release" + hm uninstall strimzi -n "$STRIMZI_NAMESPACE" --wait + fi + log "deleting Strimzi CRDs and namespace $STRIMZI_NAMESPACE" + # No Strimzi CRDs left is fine: grep exits 1 then, which must not abort under pipefail. + kc get crd -o name | { grep -E '\.strimzi\.io$' || true; } | xargs -r kubectl --context "$PROFILE" delete --ignore-not-found --wait=true + kc delete namespace "$STRIMZI_NAMESPACE" --ignore-not-found --wait=true --timeout=300s +fi + +log "cluster now:" +kc get namespaces +cat <"$RESULTS" + +selected=("$@") +[[ ${#selected[@]} -gt 0 ]] || selected=("${LANGS[@]}") +for l in "${selected[@]}"; do valid_lang "$l" || fail "unknown language '$l' (${LANGS[*]})"; done +minikube status -p "$PROFILE" >/dev/null 2>&1 || fail "profile $PROFILE is not running. Run k8s/setup-profile.sh first." + +foreign="$(docker ps --format '{{.Names}}' | grep -vx "$PROFILE" || true)" +[[ -z "$foreign" ]] || fail "containers are running (one workshop at a time): $(tr '\n' ' ' <<<"$foreign")" + +# How many distinct service names Tempo reports in a service.name values reply (0 on any error). +trace_count() { + python3 -c ' +import json, sys +try: + print(len(set(json.load(open(sys.argv[1])).get("tagValues") or []))) +except Exception: + print(0) +' "$1" +} + +evidence() { # evidence + local dir="$OUT/evidence/$1" + mkdir -p "$dir" + { + kcn get pods,svc,pvc -o wide + kcn get kafka,kafkanodepool + } >"$dir/resources.txt" 2>&1 + kcn get kafka cndp -o jsonpath='{.status.conditions}' >"$dir/kafka-status.json" 2>&1 + kcn get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}' >"$dir/images.txt" 2>&1 + local d + for d in order-service shipping-consumer notification-consumer; do + kcn logs "deployment/$d" --tail=60 >"$dir/$d.log" 2>&1 + done + # Not a verify.sh check: which services Tempo has received traces from, queried through the lgtm pod. + for _ in 1 2 3 4 5 6; do + kcn exec deploy/lgtm -- curl -s 'http://localhost:3200/api/search/tag/service.name/values' >"$dir/trace-services.json" 2>&1 + [[ "$(trace_count "$dir/trace-services.json")" -ge 3 ]] && break + sleep 5 + done + curl -s http://127.0.0.1:30300/api/health >"$dir/grafana-health.json" 2>&1 + docker port "$PROFILE" >"$dir/docker-port.txt" 2>&1 +} + +failed=0 +for lang in "${selected[@]}"; do + log=$OUT/logs/$lang.log + start=$(date +%s) + status=PASS + if ! "$K8S_DIR/build-images.sh" "$lang" >"$log" 2>&1; then + status=BUILD-FAIL + elif ! "$K8S_DIR/deploy.sh" "$lang" >>"$log" 2>&1; then + status=DEPLOY-FAIL + elif ! (cd "$EXAMPLE_DIR" && RUNTIME=kubernetes KUBE_CONTEXT="$PROFILE" KUBE_NAMESPACE="$NAMESPACE" \ + BASE=http://127.0.0.1:30080 timeout 900 ./verify.sh) >>"$log" 2>&1; then + status=VERIFY-FAIL + fi + evidence "$lang" + if [[ "$status" != PASS ]]; then failed=1; fi + "$K8S_DIR/teardown.sh" >>"$log" 2>&1 || true + secs=$(( $(date +%s) - start )) + summary="$(grep -E '==> Results:' "$log" | tail -1 | sed 's/.*Results: //' || true)" + traces="$(trace_count "$OUT/evidence/$lang/trace-services.json") services" + printf '%-12s\t%s\t%s\ttraces %s\t%ss\n' "$lang" "$status" "${summary:--}" "$traces" "$secs" | tee -a "$RESULTS" +done + +echo +column -t -s $'\t' "$RESULTS" +exit "$failed" diff --git a/examples/05-event-driven/python/compose.yaml b/examples/05-event-driven/python/compose.yaml index 9dc0e5bf..4aaa59ef 100644 --- a/examples/05-event-driven/python/compose.yaml +++ b/examples/05-event-driven/python/compose.yaml @@ -27,6 +27,7 @@ services: - DATABASE_URL=postgres://appuser:apppass@postgres:5432/appdb - KAFKA_BOOTSTRAP=kafka:9094 - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_SERVICE_NAME=order-service healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8080/healthz"] interval: 5s @@ -50,6 +51,7 @@ services: - DATABASE_URL=postgres://appuser:apppass@postgres:5432/appdb - KAFKA_BOOTSTRAP=kafka:9094 - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_SERVICE_NAME=shipping-consumer notification-consumer: build: @@ -67,3 +69,4 @@ services: - DATABASE_URL=postgres://appuser:apppass@postgres:5432/appdb - KAFKA_BOOTSTRAP=kafka:9094 - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_SERVICE_NAME=notification-consumer diff --git a/examples/05-event-driven/python/notification-consumer/main.py b/examples/05-event-driven/python/notification-consumer/main.py index 7405d26a..35b46671 100644 --- a/examples/05-event-driven/python/notification-consumer/main.py +++ b/examples/05-event-driven/python/notification-consumer/main.py @@ -7,7 +7,7 @@ import asyncpg from aiokafka import AIOKafkaConsumer -from opentelemetry import trace +from opentelemetry import propagate, trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -20,7 +20,7 @@ KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:9094") OTEL_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://lgtm:4318") -resource = Resource.create({"service.name": "notification"}) +resource = Resource.create({"service.name": os.getenv("OTEL_SERVICE_NAME", "notification-consumer")}) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{OTEL_ENDPOINT}/v1/traces")) @@ -58,7 +58,11 @@ async def main(): for msg in messages: order = msg.value order_id = order["id"] - with tracer.start_as_current_span("send_notification") as span: + # Continue the producer's trace from the Kafka headers. + parent = propagate.extract({k: v.decode() for k, v in (msg.headers or ())}) + with tracer.start_as_current_span( + "send_notification", context=parent, kind=trace.SpanKind.CONSUMER + ) as span: span.set_attribute("order.id", order_id) async with pool.acquire() as conn: try: diff --git a/examples/05-event-driven/python/order-service/main.py b/examples/05-event-driven/python/order-service/main.py index 38e0c67b..c4daf051 100644 --- a/examples/05-event-driven/python/order-service/main.py +++ b/examples/05-event-driven/python/order-service/main.py @@ -8,7 +8,7 @@ import asyncpg from aiokafka import AIOKafkaProducer from fastapi import FastAPI -from opentelemetry import trace +from opentelemetry import propagate, trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.sdk.resources import Resource @@ -23,12 +23,13 @@ KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:9094") OTEL_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://lgtm:4318") -resource = Resource.create({"service.name": "order"}) +resource = Resource.create({"service.name": os.getenv("OTEL_SERVICE_NAME", "order-service")}) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{OTEL_ENDPOINT}/v1/traces")) ) trace.set_tracer_provider(provider) +tracer = trace.get_tracer("order-service") db_pool: Optional[asyncpg.Pool] = None producer: Optional[AIOKafkaProducer] = None @@ -80,7 +81,13 @@ async def place_order(body: OrderIn): ) event = {"id": order_id, "sku": body.sku, "quantity": body.quantity, "status": "confirmed"} - await producer.send("order.placed", value=event) + # Carry the trace context in the Kafka headers so the consumer spans join this trace. + with tracer.start_as_current_span("order.placed publish", kind=trace.SpanKind.PRODUCER) as span: + span.set_attribute("order.id", order_id) + carrier: dict[str, str] = {} + propagate.inject(carrier) + headers = [(k, v.encode()) for k, v in carrier.items()] + await producer.send("order.placed", value=event, headers=headers) log.info("published order.placed id=%s", order_id) return Order(id=order_id, sku=body.sku, quantity=body.quantity, status="confirmed") diff --git a/examples/05-event-driven/python/shipping-consumer/main.py b/examples/05-event-driven/python/shipping-consumer/main.py index 263c65c3..56f12af5 100644 --- a/examples/05-event-driven/python/shipping-consumer/main.py +++ b/examples/05-event-driven/python/shipping-consumer/main.py @@ -7,7 +7,7 @@ import asyncpg from aiokafka import AIOKafkaConsumer -from opentelemetry import trace +from opentelemetry import propagate, trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -20,7 +20,7 @@ KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:9094") OTEL_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://lgtm:4318") -resource = Resource.create({"service.name": "shipping"}) +resource = Resource.create({"service.name": os.getenv("OTEL_SERVICE_NAME", "shipping-consumer")}) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{OTEL_ENDPOINT}/v1/traces")) @@ -58,7 +58,11 @@ async def main(): for msg in messages: order = msg.value order_id = order["id"] - with tracer.start_as_current_span("process_shipment") as span: + # Continue the producer's trace from the Kafka headers. + parent = propagate.extract({k: v.decode() for k, v in (msg.headers or ())}) + with tracer.start_as_current_span( + "process_shipment", context=parent, kind=trace.SpanKind.CONSUMER + ) as span: span.set_attribute("order.id", order_id) async with pool.acquire() as conn: try: diff --git a/examples/05-event-driven/quarkus/notification-consumer/src/main/resources/application.properties b/examples/05-event-driven/quarkus/notification-consumer/src/main/resources/application.properties index 9d8ad61d..2d346920 100644 --- a/examples/05-event-driven/quarkus/notification-consumer/src/main/resources/application.properties +++ b/examples/05-event-driven/quarkus/notification-consumer/src/main/resources/application.properties @@ -16,4 +16,5 @@ quarkus.log.level=INFO quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:notification-consumer} diff --git a/examples/05-event-driven/quarkus/order-service/src/main/resources/application.properties b/examples/05-event-driven/quarkus/order-service/src/main/resources/application.properties index bc29db59..c1c62298 100644 --- a/examples/05-event-driven/quarkus/order-service/src/main/resources/application.properties +++ b/examples/05-event-driven/quarkus/order-service/src/main/resources/application.properties @@ -14,4 +14,5 @@ quarkus.log.level=INFO quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:order-service} diff --git a/examples/05-event-driven/quarkus/shipping-consumer/src/main/resources/application.properties b/examples/05-event-driven/quarkus/shipping-consumer/src/main/resources/application.properties index 534a3606..cbc63675 100644 --- a/examples/05-event-driven/quarkus/shipping-consumer/src/main/resources/application.properties +++ b/examples/05-event-driven/quarkus/shipping-consumer/src/main/resources/application.properties @@ -16,4 +16,5 @@ quarkus.log.level=INFO quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:shipping-consumer} diff --git a/examples/05-event-driven/verify.sh b/examples/05-event-driven/verify.sh index 9514c2cb..562a9d9d 100755 --- a/examples/05-event-driven/verify.sh +++ b/examples/05-event-driven/verify.sh @@ -7,22 +7,57 @@ BASE="${BASE:-http://localhost:8080}" CURL_OPTS="${CURL_OPTS:-}" # Container operations go through these helpers so the checks do not hard-code -# an engine. RUNTIME=openshift is added later: a ctr_target function will map -# a container name (cndp-kafka, cndp-postgres, ...) to its pod and each helper -# will call `oc exec` / `oc logs` instead. +# an engine. RUNTIME=docker (default) calls the docker CLI on compose containers. +# RUNTIME=kubernetes calls `$KUBECTL exec` against the pods that stand in for +# those containers: ctr_target maps a container name (cndp-kafka, cndp-postgres) +# to its pod. Set KUBECTL=oc for OpenShift. +KUBECTL="${KUBECTL:-kubectl}" +KUBE_CONTEXT="${KUBE_CONTEXT:-cndp-k8s}" +KUBE_NAMESPACE="${KUBE_NAMESPACE:-cndp-05}" +KAFKA_TARGET="${KAFKA_TARGET:-pod/cndp-dual-role-0 -c kafka}" +POSTGRES_TARGET="${POSTGRES_TARGET:-pod/cndp-postgres-0}" + +unsupported() { echo "$1 is not supported on RUNTIME=$RUNTIME" >&2; return 1; } + case "$RUNTIME" in - docker) ;; - *) echo "RUNTIME=$RUNTIME not supported yet" >&2; exit 1 ;; + docker) + ctr_exec() { docker exec "$@"; } + ctr_logs() { docker logs "$@"; } + ctr_stop() { docker stop "$@"; } + ctr_start() { docker start "$@"; } + ctr_rm() { docker rm "$@"; } + ctr_kill() { docker kill --signal "${2:-KILL}" "$1"; } # ctr_kill [signal] + ctr_pause() { docker pause "$@"; } + ctr_unpause() { docker unpause "$@"; } + ctr_compose() { docker compose "$@"; } + ;; + kubernetes) + ctr_target() { + case "$1" in + cndp-kafka) echo "$KAFKA_TARGET" ;; + cndp-postgres) echo "$POSTGRES_TARGET" ;; + *) echo "no pod mapped to container $1 on RUNTIME=kubernetes" >&2; return 1 ;; + esac + } + ctr_exec() { # ctr_exec + local target + local -a args + target="$(ctr_target "$1")" || return 1 + shift + read -r -a args <<<"$target" + "$KUBECTL" --context "$KUBE_CONTEXT" -n "$KUBE_NAMESPACE" exec "${args[@]}" -- "$@" + } + ctr_logs() { unsupported ctr_logs; } + ctr_stop() { unsupported ctr_stop; } + ctr_start() { unsupported ctr_start; } + ctr_rm() { unsupported ctr_rm; } + ctr_kill() { unsupported ctr_kill; } + ctr_pause() { unsupported ctr_pause; } + ctr_unpause() { unsupported ctr_unpause; } + ctr_compose() { unsupported ctr_compose; } + ;; + *) echo "RUNTIME=$RUNTIME not supported (docker, kubernetes)" >&2; exit 1 ;; esac -ctr_exec() { docker exec "$@"; } -ctr_logs() { docker logs "$@"; } -ctr_stop() { docker stop "$@"; } -ctr_start() { docker start "$@"; } -ctr_rm() { docker rm "$@"; } -ctr_kill() { docker kill --signal "${2:-KILL}" "$1"; } # ctr_kill [signal] -ctr_pause() { docker pause "$@"; } -ctr_unpause() { docker unpause "$@"; } -ctr_compose() { docker compose "$@"; } PASS=0 FAIL=0 @@ -100,17 +135,26 @@ check "notification-group consumer group exists" \ # --- Idempotent consumers: re-deliver same event and verify no duplicates --- printf ' \xe2\x86\x92 testing idempotent consumers (re-sending duplicate event)...\n' +# A failed send must not abort the script (set -e) and must not let the two checks +# below pass vacuously: with dup_sent=0 their commands fail, so they report a failure. +dup_sent=1 +dup_was_sent() { [[ $dup_sent == 1 ]]; } DUPLICATE_EVENT="{\"id\":\"$ORDER_ID\",\"sku\":\"widget-a\",\"quantity\":5,\"status\":\"pending\"}" -ctr_exec cndp-kafka bash -c "echo '$DUPLICATE_EVENT' | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic order.placed" >/dev/null 2>&1 +ctr_exec cndp-kafka bash -c "echo '$DUPLICATE_EVENT' | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic order.placed" >/dev/null 2>&1 || dup_sent=0 sleep 8 check "shipping remains idempotent (still 1 row after duplicate)" \ - "ctr_exec cndp-postgres psql -U appuser -d appdb -tAc \"SELECT count(*) FROM shipments WHERE order_id='$ORDER_ID'\"" \ + "dup_was_sent && ctr_exec cndp-postgres psql -U appuser -d appdb -tAc \"SELECT count(*) FROM shipments WHERE order_id='$ORDER_ID'\"" \ "1" check "notification remains idempotent (still 1 row after duplicate)" \ - "ctr_exec cndp-postgres psql -U appuser -d appdb -tAc \"SELECT count(*) FROM notifications WHERE order_id='$ORDER_ID'\"" \ + "dup_was_sent && ctr_exec cndp-postgres psql -U appuser -d appdb -tAc \"SELECT count(*) FROM notifications WHERE order_id='$ORDER_ID'\"" \ "1" printf '\n==> Results: %d passed, %d failed\n' "$PASS" "$FAIL" -[[ $FAIL -eq 0 ]] && printf '==> PASS\n' || { printf '==> FAIL\n'; exit 1; } +if [[ $FAIL -eq 0 ]]; then + printf '==> PASS\n' +else + printf '==> FAIL\n' + exit 1 +fi diff --git a/examples/06-stream-processing/dotnet/Containerfile.order b/examples/06-stream-processing/dotnet/Containerfile.order index 0f957cb9..817abc21 100644 --- a/examples/06-stream-processing/dotnet/Containerfile.order +++ b/examples/06-stream-processing/dotnet/Containerfile.order @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/06-stream-processing/dotnet/Containerfile.processor b/examples/06-stream-processing/dotnet/Containerfile.processor index ea935719..e6c5ee26 100644 --- a/examples/06-stream-processing/dotnet/Containerfile.processor +++ b/examples/06-stream-processing/dotnet/Containerfile.processor @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 StreamProcessor/StreamProcessor.csproj StreamProcessor/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore StreamProcessor/StreamProcessor.csproj COPY --chown=1001:0 StreamProcessor/ StreamProcessor/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish StreamProcessor/StreamProcessor.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish StreamProcessor/StreamProcessor.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/06-stream-processing/quarkus/order-service/src/main/resources/application.properties b/examples/06-stream-processing/quarkus/order-service/src/main/resources/application.properties index bc29db59..c1c62298 100644 --- a/examples/06-stream-processing/quarkus/order-service/src/main/resources/application.properties +++ b/examples/06-stream-processing/quarkus/order-service/src/main/resources/application.properties @@ -14,4 +14,5 @@ quarkus.log.level=INFO quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:order-service} diff --git a/examples/06-stream-processing/quarkus/stream-processor/src/main/resources/application.properties b/examples/06-stream-processing/quarkus/stream-processor/src/main/resources/application.properties index 25cbe0e3..e815701e 100644 --- a/examples/06-stream-processing/quarkus/stream-processor/src/main/resources/application.properties +++ b/examples/06-stream-processing/quarkus/stream-processor/src/main/resources/application.properties @@ -22,4 +22,5 @@ quarkus.log.level=INFO quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318} +quarkus.otel.exporter.otlp.protocol=http/protobuf quarkus.application.name=${OTEL_SERVICE_NAME:stream-processor} diff --git a/examples/11-observability/dotnet/Containerfile.inventory b/examples/11-observability/dotnet/Containerfile.inventory index 964b6ffe..8b674d12 100644 --- a/examples/11-observability/dotnet/Containerfile.inventory +++ b/examples/11-observability/dotnet/Containerfile.inventory @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 InventoryService/InventoryService.csproj InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore InventoryService/InventoryService.csproj COPY --chown=1001:0 InventoryService/ InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish InventoryService/InventoryService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish InventoryService/InventoryService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/11-observability/dotnet/Containerfile.notification b/examples/11-observability/dotnet/Containerfile.notification index ffcb6d5e..cbdc4a9f 100644 --- a/examples/11-observability/dotnet/Containerfile.notification +++ b/examples/11-observability/dotnet/Containerfile.notification @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 NotificationConsumer/NotificationConsumer.csproj NotificationConsumer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore NotificationConsumer/NotificationConsumer.csproj COPY --chown=1001:0 NotificationConsumer/ NotificationConsumer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish NotificationConsumer/NotificationConsumer.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish NotificationConsumer/NotificationConsumer.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/11-observability/dotnet/Containerfile.order b/examples/11-observability/dotnet/Containerfile.order index 291ef64f..8e0a2b78 100644 --- a/examples/11-observability/dotnet/Containerfile.order +++ b/examples/11-observability/dotnet/Containerfile.order @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/12-security/dotnet/Containerfile b/examples/12-security/dotnet/Containerfile index 0f957cb9..817abc21 100644 --- a/examples/12-security/dotnet/Containerfile +++ b/examples/12-security/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/16-websockets/dotnet/Containerfile b/examples/16-websockets/dotnet/Containerfile index 89857d3c..dd855466 100644 --- a/examples/16-websockets/dotnet/Containerfile +++ b/examples/16-websockets/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 WsServer/WsServer.csproj WsServer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore WsServer/WsServer.csproj COPY --chown=1001:0 WsServer/ WsServer/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish WsServer/WsServer.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish WsServer/WsServer.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/17-sagas/dotnet/Containerfile b/examples/17-sagas/dotnet/Containerfile index de362be3..22db644c 100644 --- a/examples/17-sagas/dotnet/Containerfile +++ b/examples/17-sagas/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 SagaOrchestrator/SagaOrchestrator.csproj SagaOrchestrator/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore SagaOrchestrator/SagaOrchestrator.csproj COPY --chown=1001:0 SagaOrchestrator/ SagaOrchestrator/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish SagaOrchestrator/SagaOrchestrator.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish SagaOrchestrator/SagaOrchestrator.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/18-errors/dotnet/Containerfile.inventory b/examples/18-errors/dotnet/Containerfile.inventory index 964b6ffe..8b674d12 100644 --- a/examples/18-errors/dotnet/Containerfile.inventory +++ b/examples/18-errors/dotnet/Containerfile.inventory @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 InventoryService/InventoryService.csproj InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore InventoryService/InventoryService.csproj COPY --chown=1001:0 InventoryService/ InventoryService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish InventoryService/InventoryService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish InventoryService/InventoryService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/18-errors/dotnet/Containerfile.order b/examples/18-errors/dotnet/Containerfile.order index 291ef64f..8e0a2b78 100644 --- a/examples/18-errors/dotnet/Containerfile.order +++ b/examples/18-errors/dotnet/Containerfile.order @@ -1,11 +1,11 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src COPY --chown=1001:0 proto/ proto/ -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/19-ddd-hexagonal/dotnet/Containerfile b/examples/19-ddd-hexagonal/dotnet/Containerfile index f9b5ed5c..f26ceb62 100644 --- a/examples/19-ddd-hexagonal/dotnet/Containerfile +++ b/examples/19-ddd-hexagonal/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/21-graceful-shutdown/dotnet/Containerfile b/examples/21-graceful-shutdown/dotnet/Containerfile index 0f957cb9..817abc21 100644 --- a/examples/21-graceful-shutdown/dotnet/Containerfile +++ b/examples/21-graceful-shutdown/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/22-l7-routing/dotnet/Containerfile.order b/examples/22-l7-routing/dotnet/Containerfile.order index 0f957cb9..817abc21 100644 --- a/examples/22-l7-routing/dotnet/Containerfile.order +++ b/examples/22-l7-routing/dotnet/Containerfile.order @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 OrderService/OrderService.csproj OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore OrderService/OrderService.csproj COPY --chown=1001:0 OrderService/ OrderService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish OrderService/OrderService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish OrderService/OrderService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/22-l7-routing/dotnet/Containerfile.router b/examples/22-l7-routing/dotnet/Containerfile.router index c8fb88b8..df3f7ee1 100644 --- a/examples/22-l7-routing/dotnet/Containerfile.router +++ b/examples/22-l7-routing/dotnet/Containerfile.router @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 RouterService/RouterService.csproj RouterService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore RouterService/RouterService.csproj COPY --chown=1001:0 RouterService/ RouterService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish RouterService/RouterService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish RouterService/RouterService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/24-monolith-to-microservices/dotnet/Containerfile.decorator b/examples/24-monolith-to-microservices/dotnet/Containerfile.decorator index 646e3d91..44353b58 100644 --- a/examples/24-monolith-to-microservices/dotnet/Containerfile.decorator +++ b/examples/24-monolith-to-microservices/dotnet/Containerfile.decorator @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 DecoratorService/DecoratorService.csproj DecoratorService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore DecoratorService/DecoratorService.csproj COPY --chown=1001:0 DecoratorService/ DecoratorService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish DecoratorService/DecoratorService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish DecoratorService/DecoratorService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/24-monolith-to-microservices/dotnet/Containerfile.router b/examples/24-monolith-to-microservices/dotnet/Containerfile.router index c8fb88b8..df3f7ee1 100644 --- a/examples/24-monolith-to-microservices/dotnet/Containerfile.router +++ b/examples/24-monolith-to-microservices/dotnet/Containerfile.router @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 RouterService/RouterService.csproj RouterService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore RouterService/RouterService.csproj COPY --chown=1001:0 RouterService/ RouterService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish RouterService/RouterService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish RouterService/RouterService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/24-monolith-to-microservices/dotnet/Containerfile.stub b/examples/24-monolith-to-microservices/dotnet/Containerfile.stub index 8bbcc5cc..b820e433 100644 --- a/examples/24-monolith-to-microservices/dotnet/Containerfile.stub +++ b/examples/24-monolith-to-microservices/dotnet/Containerfile.stub @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 StubService/StubService.csproj StubService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore StubService/StubService.csproj COPY --chown=1001:0 StubService/ StubService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish StubService/StubService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish StubService/StubService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/25-caching/dotnet/Containerfile b/examples/25-caching/dotnet/Containerfile index ef5dbe4e..86efa5e7 100644 --- a/examples/25-caching/dotnet/Containerfile +++ b/examples/25-caching/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 CacheService/CacheService.csproj CacheService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore CacheService/CacheService.csproj COPY --chown=1001:0 CacheService/ CacheService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish CacheService/CacheService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish CacheService/CacheService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/26-failure-modes/dotnet/Containerfile.backend b/examples/26-failure-modes/dotnet/Containerfile.backend index 91aa9e65..bd02b0c3 100644 --- a/examples/26-failure-modes/dotnet/Containerfile.backend +++ b/examples/26-failure-modes/dotnet/Containerfile.backend @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 BackendService/BackendService.csproj BackendService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore BackendService/BackendService.csproj COPY --chown=1001:0 BackendService/ BackendService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish BackendService/BackendService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish BackendService/BackendService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/26-failure-modes/dotnet/Containerfile.edge b/examples/26-failure-modes/dotnet/Containerfile.edge index 6c0956d7..a5d0e603 100644 --- a/examples/26-failure-modes/dotnet/Containerfile.edge +++ b/examples/26-failure-modes/dotnet/Containerfile.edge @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 EdgeService/EdgeService.csproj EdgeService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore EdgeService/EdgeService.csproj COPY --chown=1001:0 EdgeService/ EdgeService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish EdgeService/EdgeService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish EdgeService/EdgeService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/27-feature-flags/dotnet/Containerfile b/examples/27-feature-flags/dotnet/Containerfile index 69fd7c29..f6bde8ca 100644 --- a/examples/27-feature-flags/dotnet/Containerfile +++ b/examples/27-feature-flags/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 FlagService/FlagService.csproj FlagService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore FlagService/FlagService.csproj COPY --chown=1001:0 FlagService/ FlagService/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish FlagService/FlagService.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish FlagService/FlagService.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/30-blazor-signalr/dotnet/Containerfile b/examples/30-blazor-signalr/dotnet/Containerfile index b0082c04..a4094b90 100644 --- a/examples/30-blazor-signalr/dotnet/Containerfile +++ b/examples/30-blazor-signalr/dotnet/Containerfile @@ -1,10 +1,10 @@ FROM registry.access.redhat.com/ubi9/dotnet-100:9.8-1791421718 AS build +# Restore and publish share one RUN so the NuGet cache mount is always present when packages are used; +# sharing=locked keeps two images built in parallel from extracting into the same package directory. ENV NUGET_PACKAGES=/nuget-cache WORKDIR /opt/app-root/src -COPY --chown=1001:0 BlazorDashboard/BlazorDashboard.csproj BlazorDashboard/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet restore BlazorDashboard/BlazorDashboard.csproj COPY --chown=1001:0 BlazorDashboard/ BlazorDashboard/ -RUN --mount=type=cache,target=/nuget-cache,uid=1001 dotnet publish BlazorDashboard/BlazorDashboard.csproj -c Release --no-restore -o /opt/app-root/app +RUN --mount=type=cache,target=/nuget-cache,uid=1001,sharing=locked dotnet publish BlazorDashboard/BlazorDashboard.csproj -c Release -o /opt/app-root/app FROM registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 COPY --from=build --chown=1001:0 --chmod=g=u /opt/app-root/app /opt/app-root/app diff --git a/examples/PREREQUISITES.md b/examples/PREREQUISITES.md index b3bd561e..ef3b2ce1 100644 --- a/examples/PREREQUISITES.md +++ b/examples/PREREQUISITES.md @@ -73,21 +73,33 @@ docker run --rm hello-world ## Kubernetes tools (optional) -The examples run on `docker compose` and need no cluster. A later part of the tutorial adds -optional minikube and OpenShift Local paths; install these tools only when you reach them. +The examples run on `docker compose` and need no cluster. Appendix R runs Example 05 on +minikube; install these tools when you reach it. minikube uses the Docker driver on the same +Docker Engine as compose, with the `default` Docker context. ### minikube +Pinned to v1.39.0, with the binary checked against its published SHA-256 file: + ```bash -curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 -sudo install minikube-linux-amd64 /usr/local/bin/minikube +MINIKUBE_VERSION=v1.39.0 +mkdir -p ~/.local/bin && cd "$(mktemp -d)" +curl -fsSLO "https://github.com/kubernetes/minikube/releases/download/${MINIKUBE_VERSION}/minikube-linux-amd64" +echo "$(curl -fsSL "https://github.com/kubernetes/minikube/releases/download/${MINIKUBE_VERSION}/minikube-linux-amd64.sha256") minikube-linux-amd64" | sha256sum -c - +install minikube-linux-amd64 ~/.local/bin/minikube ``` ### kubectl +Pinned to v1.36.5 (the Kubernetes version the minikube profile runs), checked against its +published SHA-256: + ```bash -curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" -sudo install kubectl /usr/local/bin/kubectl +KUBECTL_VERSION=v1.36.5 +mkdir -p ~/.local/bin && cd "$(mktemp -d)" +curl -fsSLO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" +echo "$(curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256") kubectl" | sha256sum -c - +install kubectl ~/.local/bin/kubectl ``` ### Helm @@ -235,7 +247,7 @@ container. | Stack | Memory | Disk | CPU | |-------|--------|------|-----| | Docker compose examples | ~2-4 GB | ~3 GB for images | 2+ cores | -| Minikube (optional) | ~6-8 GB | ~10 GB | 4+ cores | +| Minikube, Example 05 (optional) | 8 GB for the node | 40 GB (profile disk) | 4+ cores | The LGTM observability stack (Grafana, Loki, Tempo, Mimir) is the heaviest shared component at ~1.5 GB. diff --git a/index.html b/index.html index 9da69751..f4a0edd8 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,7 @@

{{ site.title }}

6One book, six languages — Spring Boot, Quarkus, .NET, Python, C++, Go — behind one tab
-
13Chapters from cloud-native principles through anti-patterns, plus seventeen deep-dive appendices A–Q and an acronym glossary
+
13Chapters from cloud-native principles through anti-patterns, plus eighteen deep-dive appendices A–R and an acronym glossary
📊Runs on Docker Engine locally and plain Kubernetes — no managed cloud on the primary path
21Runnable examples with automated verification — Spring Boot, Quarkus, .NET, Python, C++, and Go
diff --git a/scripts/ci_changed_projects.py b/scripts/ci_changed_projects.py index e4d18b42..33f1c5dd 100755 --- a/scripts/ci_changed_projects.py +++ b/scripts/ci_changed_projects.py @@ -16,7 +16,8 @@ python directories containing requirements.txt cpp directories containing conanfile.py (each also has a Containerfile) compose compose*.yaml files - scripts verify.sh files + scripts verify.sh files and examples//k8s/*.sh + k8s examples//k8s directories (kustomize manifests, rendered and validated) Everything is selected when the base is unknown (workflow_dispatch, all-zero `before` SHA of a first push, a base that cannot be diffed) and when a shared @@ -59,11 +60,13 @@ "python": ("python",), "cpp": ("cpp",), } -KINDS = list(MARKERS) + ["compose", "scripts"] +KINDS = list(MARKERS) + ["compose", "scripts", "k8s"] ZERO_SHA = re.compile(r"^0+$") # Changes that cannot affect a build or a config check. INERT = ("*.md", "*.svg", "*.excalidraw", "*.png") INERT_DIRS = ("db", "collections", "grafana", "otelcol") +# examples//k8s holds kustomize manifests and the scripts that drive minikube. +K8S_DIR = "k8s" # Files under examples/_infra/ are fragments pulled in by the examples' compose @@ -78,6 +81,21 @@ def is_compose(path: str) -> bool: and not path.startswith(COMPOSE_FRAGMENT_DIR)) +def is_k8s_script(path: str) -> bool: + parts = path.split("/") + return len(parts) >= 4 and parts[2] == K8S_DIR and path.endswith(".sh") + + +def k8s_dirs(files: list[str]) -> set[str]: + """examples//k8s directories among tracked files.""" + out = set() + for f in files: + parts = f.split("/") + if len(parts) >= 4 and parts[0] == EXAMPLES and parts[2] == K8S_DIR: + out.add("/".join(parts[:3])) + return out + + def git(*args: str) -> str: return subprocess.run(["git", "-C", str(ROOT), *args], check=True, text=True, capture_output=True).stdout @@ -113,7 +131,8 @@ def select_everything() -> dict[str, set[str]]: files = tracked() sel = {k: all_projects(k, files) for k in MARKERS} sel["compose"] = {f for f in files if is_compose(f)} - sel["scripts"] = {f for f in files if os.path.basename(f) == "verify.sh"} + sel["scripts"] = {f for f in files if os.path.basename(f) == "verify.sh" or is_k8s_script(f)} + sel["k8s"] = k8s_dirs(files) return sel @@ -143,6 +162,21 @@ def select_for_changes(changed: list[str]) -> dict[str, set[str]]: if exists: sel["scripts"].add(f) continue + # The schema is rendered into the k8s manifests, so a change to it selects them too. + if len(parts) >= 4 and parts[2] == "db": + k8s = f"{parts[0]}/{parts[1]}/{K8S_DIR}" + if k8s in k8s_dirs(files): + sel["k8s"].add(k8s) + if len(parts) >= 4 and parts[2] == K8S_DIR: + if is_k8s_script(f): + if exists: + sel["scripts"].add(f) + elif not name.endswith(".md"): + # A deleted manifest still selects the directory, if it still exists. + k8s = "/".join(parts[:3]) + if k8s in k8s_dirs(files): + sel["k8s"].add(k8s) + continue if any(fnmatch.fnmatch(name, pat) for pat in INERT) or any(p in INERT_DIRS for p in parts[2:-1]): continue hit = False diff --git a/scripts/gen_29_diagrams.py b/scripts/gen_29_diagrams.py index 8f7fc6d6..381375e5 100644 --- a/scripts/gen_29_diagrams.py +++ b/scripts/gen_29_diagrams.py @@ -139,9 +139,9 @@ {"x": 230, "y": 55, "w": 160, "h": 55, "style": "kernel", "lines": ["SDK tools", "MSBuild, NuGet, Roslyn"]}, {"x": 30, "y": 140, "w": 360, "h": 50, "style": "box", - "lines": ["COPY *.csproj → dotnet restore", "download NuGet packages (cached layer)"]}, + "lines": ["COPY source", "NuGet package cache is a BuildKit cache mount"]}, {"x": 30, "y": 210, "w": 360, "h": 50, "style": "box", - "lines": ["COPY source → dotnet publish -c Release", "compile C# → CIL, tree-shake deps"]}, + "lines": ["dotnet publish -c Release", "restore from the mount, compile C# → CIL"]}, {"x": 30, "y": 290, "w": 170, "h": 55, "style": "ink", "lines": ["/app output", "*.dll + deps", "~30–80 MB"]}, # Trimming note diff --git a/scripts/gen_31_diagrams.py b/scripts/gen_31_diagrams.py new file mode 100644 index 00000000..81319f52 --- /dev/null +++ b/scripts/gen_31_diagrams.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Generate diagrams for Appendix R: Running Example 05 on minikube.""" +import generate_diagram as g +g.OUT = "../assets/diagrams" + +# ── Figure R.1 — cluster topology and host access ─────────────────────── +g.emit("31-minikube-topology", 880, 420, + bands=[ + {"x": 10, "y": 10, "w": 200, "h": 400, "fill": "#eef4fb", + "label": "Host (Docker Engine)"}, + {"x": 230, "y": 10, "w": 640, "h": 400, "fill": "#f7f7f7", + "label": "Node container cndp-k8s (containerd, Kubernetes v1.36.5)"}, + {"x": 250, "y": 40, "w": 160, "h": 360, "fill": "#ffffff", + "label": "namespace strimzi"}, + {"x": 430, "y": 40, "w": 425, "h": 360, "fill": "#fff8ef", + "label": "namespace cndp-05"}, + ], + nodes=[ + {"x": 25, "y": 60, "w": 170, "h": 55, "style": "user", + "lines": ["curl, verify.sh", "127.0.0.1:30080"]}, + {"x": 25, "y": 135, "w": 170, "h": 55, "style": "user", + "lines": ["Browser", "127.0.0.1:30300"]}, + {"x": 25, "y": 210, "w": 170, "h": 55, "style": "kernel", + "lines": ["kubectl --context", "cndp-k8s"]}, + {"x": 25, "y": 285, "w": 170, "h": 55, "style": "ghost", + "lines": ["Other profiles", "stopped"]}, + {"x": 265, "y": 310, "w": 130, "h": 60, "style": "accent", + "lines": ["Strimzi operator", "Helm chart 1.2.0"]}, + {"x": 450, "y": 80, "w": 190, "h": 55, "style": "ink", + "lines": ["order-service", "NodePort 30080"]}, + {"x": 660, "y": 80, "w": 180, "h": 55, "style": "box", + "lines": ["shipping-consumer", "notification-consumer"]}, + {"x": 450, "y": 180, "w": 190, "h": 60, "style": "accent", + "lines": ["Kafka cndp (KRaft)", "cndp-kafka-bootstrap:9092"]}, + {"x": 660, "y": 180, "w": 180, "h": 60, "style": "accent", + "lines": ["Postgres 18", "cndp-postgres-rw:5432"]}, + {"x": 450, "y": 285, "w": 190, "h": 60, "style": "kernel", + "lines": ["LGTM (otel-lgtm)", "lgtm:4318, Grafana 30300"]}, + {"x": 660, "y": 285, "w": 180, "h": 60, "style": "sub", + "lines": ["Secret cndp-postgres-app", "PG_USER, PG_PASSWORD"]}, + ], + edges=[ + {"x1": 195, "y1": 87, "x2": 450, "y2": 107, "amber": True}, + {"x1": 195, "y1": 162, "x2": 450, "y2": 315, "amber": True}, + {"x1": 195, "y1": 237, "x2": 450, "y2": 215, "dashed": True, "label": "exec", "lx": 0}, + {"x1": 395, "y1": 335, "x2": 450, "y2": 225, "dashed": True, "label": "manages", "lx": -4}, + {"x1": 545, "y1": 135, "x2": 545, "y2": 180, "label": "produce"}, + {"x1": 750, "y1": 135, "x2": 750, "y2": 180, "label": "insert"}, + {"x1": 640, "y1": 208, "x2": 660, "y2": 208}, + ], + notes=[ + {"x": 25, "y": 395, "text": "Both ports are published on 127.0.0.1 when the profile is created. No port-forward, tunnel or minikube service.", + "size": 11, "color": "#888888"}, + ], +) + +# ── Figure R.2 — image flow ───────────────────────────────────────────── +g.emit("31-minikube-image-flow", 880, 300, + bands=[ + {"x": 10, "y": 10, "w": 540, "h": 190, "fill": "#eef4fb", + "label": "Host"}, + {"x": 570, "y": 10, "w": 300, "h": 190, "fill": "#fff8ef", + "label": "Node container cndp-k8s"}, + ], + nodes=[ + {"x": 30, "y": 55, "w": 150, "h": 55, "style": "box", + "lines": ["Containerfile", "per language, per service"]}, + {"x": 210, "y": 55, "w": 150, "h": 55, "style": "accent", + "lines": ["docker build", "host BuildKit cache"]}, + {"x": 390, "y": 55, "w": 140, "h": 55, "style": "user", + "lines": ["Docker image store", "localhost/cndp-05/..."]}, + {"x": 590, "y": 55, "w": 130, "h": 55, "style": "kernel", + "lines": ["containerd", "same image name"]}, + {"x": 740, "y": 55, "w": 110, "h": 55, "style": "ink", + "lines": ["Pod", "pullPolicy: Never"]}, + {"x": 390, "y": 140, "w": 140, "h": 40, "style": "sub", + "lines": ["tag :v1, no :latest"]}, + {"x": 30, "y": 230, "w": 820, "h": 45, "style": "ghost", + "lines": ["Rejected: minikube image build (discards the host caches), a registry addon (extra moving part)"]}, + ], + edges=[ + {"x1": 180, "y1": 82, "x2": 210, "y2": 82}, + {"x1": 360, "y1": 82, "x2": 390, "y2": 82}, + {"x1": 530, "y1": 82, "x2": 590, "y2": 82, "amber": True, "label": "image load", "lx": 0, "ly": -14}, + {"x1": 720, "y1": 82, "x2": 740, "y2": 82}, + ], + notes=[], +) +print("Done — generated 2 diagrams for Appendix R")