From d1e4298786ba00db7525f72d5233271ad9dd947b Mon Sep 17 00:00:00 2001 From: Alex Creasy Date: Wed, 29 Jul 2026 15:03:14 +0100 Subject: [PATCH 1/2] Adds interactive content for lesson 2: promoting from stage to production Signed-off-by: Alex Creasy --- lessons/02-lesson-2/README.md | 444 ++++++++++++++++++ .../lesson-manifests/base/combined-pool.yaml | 25 + .../lesson-manifests/base/kafka.yaml | 28 ++ .../lesson-manifests/base/kustomization.yaml | 5 + .../overlays/production/kustomization.yaml | 6 + .../overlays/production/namespace.yaml | 6 + .../overlays/production/topic.yaml | 12 + .../overlays/staging/kustomization.yaml | 7 + .../overlays/staging/namespace.yaml | 6 + .../overlays/staging/topic.yaml | 12 + lessons/02-lesson-2/prep.sh | 227 +++++++++ lessons/README.md | 1 + 12 files changed, 779 insertions(+) create mode 100644 lessons/02-lesson-2/README.md create mode 100644 lessons/02-lesson-2/lesson-manifests/base/combined-pool.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/base/kafka.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/base/kustomization.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/overlays/production/kustomization.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/overlays/production/namespace.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/overlays/production/topic.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/overlays/staging/kustomization.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/overlays/staging/namespace.yaml create mode 100644 lessons/02-lesson-2/lesson-manifests/overlays/staging/topic.yaml create mode 100755 lessons/02-lesson-2/prep.sh diff --git a/lessons/02-lesson-2/README.md b/lessons/02-lesson-2/README.md new file mode 100644 index 0000000..aaeb785 --- /dev/null +++ b/lessons/02-lesson-2/README.md @@ -0,0 +1,444 @@ +# Lesson 2: Promoting Changes Across Environments + +**Series:** GitOps with StreamsHub — 3-part series +**Prerequisites:** Complete [Getting Started](../00-setup/README.md) first +**Time:** ~25 minutes (plus ~8 minutes for first-time setup, ~5 minutes for prep) + +--- + +## What you will learn + +By the end of this lesson you will understand: + +- How **kustomize overlays** let you share a base configuration and layer environment-specific differences on top +- How ArgoCD can manage multiple Applications from a single Git repository, each watching a different path +- What it means to **promote** a change from staging to production — and why it is just a Git change + +You will do this by observing a staging environment with a deployed Kafka topic, then promoting that topic to production by editing a single file and pushing to Git. + +--- + +## Prerequisites + +You need the following tools installed on your machine: + +| Tool | Purpose | Install | +|------|---------|---------| +| **Docker** | Container runtime | [docker.com](https://www.docker.com/products/docker-desktop/) | +| **KinD** (v0.20+) | Local Kubernetes clusters | `brew install kind` or [kind.sigs.k8s.io](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) | +| **kubectl** | Kubernetes CLI | `brew install kubectl` or [kubernetes.io](https://kubernetes.io/docs/tasks/tools/) | +| **git** | Version control | `brew install git` or [git-scm.com](https://git-scm.com/) | +| **curl** | HTTP requests | Usually pre-installed | + +**System requirements:** ~6 GB of available memory for Docker (this lesson runs two Kafka clusters). + +You also need the shared tutorial infrastructure running. If you haven't done this yet: + +```bash +cd ../00-setup && ./setup.sh +``` + +--- + +## Background: Why environments matter + +In Lesson 1 you made a single change and watched it reach the cluster automatically. In practice, organisations don't push changes directly to production — they promote changes through a chain of environments: developers push to **staging** first, validate the change, then promote to **production**. + +The key insight: promotion in a GitOps world is not a deploy command. It is a Git change. You describe what each environment should look like in Git, and ArgoCD continuously reconciles each environment to match its description. Promoting a change means updating the description for the target environment and pushing. + +**Kustomize overlays** are the standard mechanism for this. You keep a shared base configuration and then have one overlay per environment that references the base and adds or patches environment-specific resources. ArgoCD points a separate Application at each overlay. + +--- + +## Setup + +Run the prep script from this directory: + +```bash +./prep.sh +``` + +This takes approximately 5 minutes. It: + +1. Removes the Lesson 1 state from the cluster +2. Configures the Strimzi operator to watch the new namespaces +3. Seeds the Gitea repository with the multi-environment overlay structure +4. Creates two ArgoCD Applications — one for staging and one for production +5. Waits for both Kafka clusters to become ready + +When it finishes it prints the Gitea and ArgoCD credentials. + +You can re-run `./prep.sh` at any time to reset back to the lesson starting state. + +--- + +## Part 1: Explore the environment + +### Clone the repository + +```bash +git clone http://tutorial-user:tutorial-password@localhost:3001/tutorial-user/streamshub-gitops.git /tmp/gitops-lesson-2 +cd /tmp/gitops-lesson-2 +``` + +### Check what's running in each environment + +```bash +kubectl get kafka -n kafka-staging +kubectl get kafka -n kafka-production +``` + +Both should show `READY: True` — you have two independent Kafka clusters, each in its own namespace. + +Now check for topics: + +```bash +kubectl get kafkatopic -n kafka-staging +kubectl get kafkatopic -n kafka-production +``` + +You should see `my-first-topic` in staging, but nothing in production. **This is the starting state: staging is ahead of production.** + +### Check the ArgoCD Applications + +```bash +kubectl get application -n argocd +``` + +You'll see two Applications: `kafka-staging` and `kafka-production`. Each one watches a different path in the same Git repository, and each deploys to a different namespace. + +--- + +## Part 2: Understand the overlay structure + +Look at how the repository is organised: + +```bash +ls manifests/ +``` + +Instead of a flat `manifests/` directory as in Lesson 1, you'll see: + +``` +manifests/ +├── base/ +│ ├── kustomization.yaml +│ ├── kafka.yaml +│ └── combined-pool.yaml +└── overlays/ + ├── staging/ + │ ├── kustomization.yaml + │ ├── namespace.yaml + │ └── topic.yaml + └── production/ + ├── kustomization.yaml + ├── namespace.yaml + └── topic.yaml +``` + +### The base + +```bash +cat manifests/base/kustomization.yaml +``` + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - combined-pool.yaml + - kafka.yaml +``` + +The base contains the shared Kafka cluster definition. It has no namespace or environment-specific configuration — those come from the overlays. + +### The staging overlay + +```bash +cat manifests/overlays/staging/kustomization.yaml +``` + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kafka-staging +resources: + - ../../base + - namespace.yaml + - topic.yaml +``` + +The staging overlay: +- Sets `namespace: kafka-staging` — kustomize applies this to every resource from the base +- Includes the base (shared Kafka config) +- Adds its own `namespace.yaml` (to create the `kafka-staging` namespace) +- Adds `topic.yaml` (the Kafka topic) + +The ArgoCD `kafka-staging` Application points to this directory. When kustomize renders it, ArgoCD gets the full set of resources: namespace, Kafka cluster, KafkaNodePool, and topic — all in the `kafka-staging` namespace. + +### The production overlay + +```bash +cat manifests/overlays/production/kustomization.yaml +``` + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kafka-production +resources: + - ../../base + - namespace.yaml +``` + +Notice that `topic.yaml` is **absent** from the resources list — even though the file exists: + +```bash +ls manifests/overlays/production/ +``` + +`topic.yaml` is there, waiting. But because it is not in `kustomization.yaml`, ArgoCD ignores it. The production Kafka cluster is running, but no topic has been promoted to it yet. + +This is the same pattern as Lesson 1 — a file existing in the repository does not mean it is deployed. Only what appears in `kustomization.yaml` gets deployed. + +--- + +## Part 3: Promote the topic to production + +Your staging team has validated `my-first-topic` and it is ready for production. Promoting it is a single Git change. + +Open `manifests/overlays/production/kustomization.yaml` in your editor and add `- topic.yaml` to the resources list: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kafka-production +resources: + - ../../base + - namespace.yaml + - topic.yaml +``` + +Save, commit, and push: + +```bash +git add manifests/overlays/production/kustomization.yaml +git commit -m "Promote my-first-topic to production" +git push +``` + +That is the promotion. You changed Git; the system will reconcile to match. + +--- + +## Part 4: Watch both ArgoCD Applications + +Watch the production Application detect and apply your change: + +```bash +kubectl get application kafka-production -n argocd -w +``` + +The `SYNC STATUS` column will move from `Synced` → `OutOfSync` → `Synced`. Press `Ctrl+C` once it settles. Meanwhile, open a second terminal and confirm staging remained untouched: + +```bash +kubectl get application kafka-staging -n argocd +``` + +`kafka-staging` stays `Synced` throughout — you only changed the production overlay, so only the production Application was affected. + +### Verify the topic is in production + +Once `kafka-production` shows `Synced`: + +```bash +kubectl get kafkatopic -n kafka-production +``` + +Expected output: + +``` +NAME CLUSTER PARTITIONS REPLICATION FACTOR READY +my-first-topic my-cluster 3 1 True +``` + +Confirm staging is unchanged: + +```bash +kubectl get kafkatopic -n kafka-staging +``` + +Same topic, same configuration. **You promoted a change from staging to production by pushing a single-line Git change.** + +--- + +## How it worked + +``` +git push + └─▶ Gitea receives the commit + +ArgoCD polls Gitea every 3 minutes + └─▶ kafka-staging Application: manifests/overlays/staging → no change → stays Synced + └─▶ kafka-production Application: manifests/overlays/production → topic.yaml now included + └─▶ ArgoCD applies the diff to the kafka-production namespace — creating the KafkaTopic resource + +Strimzi Topic Operator (watching kafka-production) + └─▶ Strimzi sees the new KafkaTopic and creates the topic inside the kafka-production Kafka broker +``` + +Each Application is independent. Changes to one overlay do not affect the other. The Git repository is the source of truth for both environments, and the overlay structure makes clear exactly what each environment contains. + +--- + +## Optional: View the ArgoCD dashboard + +In a separate terminal, start the port-forward: + +```bash +kubectl port-forward svc/argocd-server -n argocd 8080:443 +``` + +Open [https://localhost:8080](https://localhost:8080) (accept the self-signed certificate warning). + +Retrieve the admin password: + +```bash +kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d; echo +``` + +Log in with username `admin`. You will see both `kafka-staging` and `kafka-production` Applications. Click each one to see its resource tree — the resources are the same (Namespace, KafkaNodePool, Kafka), but one includes a KafkaTopic and the other does not. + +--- + +## Optional: Force an immediate sync + +To trigger ArgoCD without waiting up to 3 minutes: + +```bash +kubectl annotate application kafka-production -n argocd \ + argocd.argoproj.io/refresh=normal --overwrite +``` + +--- + +## Bonus: Environment-specific configuration + +So far you promoted `my-first-topic` to production with exactly the same configuration as staging — 3 partitions. In the real world, production often needs a different configuration: more partitions for throughput, a longer retention period, higher replication. + +You can patch resources in an overlay by simply editing the overlay's copy of the file. + +Open `manifests/overlays/production/topic.yaml` and increase the partition count to match production-scale requirements: + +```yaml +apiVersion: kafka.strimzi.io/v1 +kind: KafkaTopic +metadata: + name: my-first-topic + labels: + strimzi.io/cluster: my-cluster +spec: + partitions: 10 + replicas: 1 + config: + retention.ms: "86400000" + segment.bytes: "1073741824" +``` + +Commit and push: + +```bash +git add manifests/overlays/production/topic.yaml +git commit -m "Set production topic to 10 partitions" +git push +``` + +After ArgoCD syncs, verify the partition counts in each environment: + +```bash +kubectl get kafkatopic my-first-topic -n kafka-production -o jsonpath='{.spec.partitions}'; echo +kubectl get kafkatopic my-first-topic -n kafka-staging -o jsonpath='{.spec.partitions}'; echo +``` + +Production shows `10`; staging still shows `3`. **The environments are independently configurable** — a change to one overlay has no effect on the other. + +--- + +## What you've learned + +- Kustomize overlays let you share a base configuration and layer environment-specific changes on top without duplicating files +- ArgoCD can manage multiple Applications from a single Git repository, each watching a different path +- Promotion is a Git change — adding a resource to the target environment's `kustomization.yaml` is all it takes +- Environments are isolated from each other: a change to one overlay does not affect others +- In production, you would typically use separate clusters or ArgoCD instances per environment; the promotion principle is identical — it is always a Git change that drives the sync + +--- + +## Cleanup + +When you are done with all lessons, delete the cluster: + +```bash +../00-setup/teardown.sh +``` + +Clean up the cloned repo: + +```bash +rm -rf /tmp/gitops-lesson-2 +``` + +--- + +## What's next + +In **Lesson 3: Rolling Back a Bad Change**, you will use `git revert` to undo a broken configuration that has already reached production — and watch GitOps automatically restore the cluster to the last known-good state. + +--- + +## Troubleshooting + +**Infrastructure is not running** +If `./prep.sh` reports that the cluster or Strimzi is not found, run the setup script first: `../00-setup/setup.sh`. + +**Kafka clusters are not becoming ready** +Both clusters start in parallel. Check pod status in each namespace: + +```bash +kubectl get pods -n kafka-staging +kubectl get pods -n kafka-production +``` + +If pods are in `Pending` state, your Docker memory may be insufficient. This lesson requires ~6 GB. Check Docker Desktop's memory settings. + +**ArgoCD Application is not syncing** +Check for error messages: + +```bash +kubectl get application kafka-production -n argocd -o yaml +``` + +If Gitea is unreachable from inside the cluster: + +```bash +kubectl get pods -n gitea +``` + +**Topic is not appearing after sync** +Confirm your edit was committed correctly: + +```bash +git log --oneline -3 +git show HEAD:manifests/overlays/production/kustomization.yaml +``` + +Confirm `- topic.yaml` appears in the resources list. + +**Strimzi is not managing the Kafka clusters** +Check the operator logs to confirm it is watching both namespaces: + +```bash +kubectl logs deployment/strimzi-cluster-operator -n strimzi-operator | grep STRIMZI_NAMESPACE +``` + +You should see `kafka-staging,kafka-production`. diff --git a/lessons/02-lesson-2/lesson-manifests/base/combined-pool.yaml b/lessons/02-lesson-2/lesson-manifests/base/combined-pool.yaml new file mode 100644 index 0000000..dc1e937 --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/base/combined-pool.yaml @@ -0,0 +1,25 @@ +apiVersion: kafka.strimzi.io/v1 +kind: KafkaNodePool +metadata: + name: combined + labels: + strimzi.io/cluster: my-cluster +spec: + replicas: 1 + roles: + - controller + - broker + storage: + type: jbod + volumes: + - id: 0 + type: persistent-claim + size: 2Gi + deleteClaim: true + resources: + requests: + memory: 512Mi + cpu: 250m + limits: + memory: 1Gi + cpu: 500m diff --git a/lessons/02-lesson-2/lesson-manifests/base/kafka.yaml b/lessons/02-lesson-2/lesson-manifests/base/kafka.yaml new file mode 100644 index 0000000..e4774cb --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/base/kafka.yaml @@ -0,0 +1,28 @@ +apiVersion: kafka.strimzi.io/v1 +kind: Kafka +metadata: + name: my-cluster +spec: + kafka: + version: 4.2.0 + metadataVersion: "4.2-IV0" + listeners: + - name: plain + port: 9092 + type: internal + tls: false + config: + offsets.topic.replication.factor: 1 + transaction.state.log.replication.factor: 1 + transaction.state.log.min.isr: 1 + default.replication.factor: 1 + min.insync.replicas: 1 + entityOperator: + topicOperator: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 250m + memory: 384Mi diff --git a/lessons/02-lesson-2/lesson-manifests/base/kustomization.yaml b/lessons/02-lesson-2/lesson-manifests/base/kustomization.yaml new file mode 100644 index 0000000..8f893ff --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/base/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - combined-pool.yaml + - kafka.yaml diff --git a/lessons/02-lesson-2/lesson-manifests/overlays/production/kustomization.yaml b/lessons/02-lesson-2/lesson-manifests/overlays/production/kustomization.yaml new file mode 100644 index 0000000..1dde6d6 --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/overlays/production/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kafka-production +resources: + - ../../base + - namespace.yaml diff --git a/lessons/02-lesson-2/lesson-manifests/overlays/production/namespace.yaml b/lessons/02-lesson-2/lesson-manifests/overlays/production/namespace.yaml new file mode 100644 index 0000000..658c810 --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/overlays/production/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kafka-production + labels: + name: kafka-production diff --git a/lessons/02-lesson-2/lesson-manifests/overlays/production/topic.yaml b/lessons/02-lesson-2/lesson-manifests/overlays/production/topic.yaml new file mode 100644 index 0000000..41d03b4 --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/overlays/production/topic.yaml @@ -0,0 +1,12 @@ +apiVersion: kafka.strimzi.io/v1 +kind: KafkaTopic +metadata: + name: my-first-topic + labels: + strimzi.io/cluster: my-cluster +spec: + partitions: 3 + replicas: 1 + config: + retention.ms: "86400000" + segment.bytes: "1073741824" diff --git a/lessons/02-lesson-2/lesson-manifests/overlays/staging/kustomization.yaml b/lessons/02-lesson-2/lesson-manifests/overlays/staging/kustomization.yaml new file mode 100644 index 0000000..9cb3700 --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/overlays/staging/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kafka-staging +resources: + - ../../base + - namespace.yaml + - topic.yaml diff --git a/lessons/02-lesson-2/lesson-manifests/overlays/staging/namespace.yaml b/lessons/02-lesson-2/lesson-manifests/overlays/staging/namespace.yaml new file mode 100644 index 0000000..c2c01ea --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/overlays/staging/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kafka-staging + labels: + name: kafka-staging diff --git a/lessons/02-lesson-2/lesson-manifests/overlays/staging/topic.yaml b/lessons/02-lesson-2/lesson-manifests/overlays/staging/topic.yaml new file mode 100644 index 0000000..41d03b4 --- /dev/null +++ b/lessons/02-lesson-2/lesson-manifests/overlays/staging/topic.yaml @@ -0,0 +1,12 @@ +apiVersion: kafka.strimzi.io/v1 +kind: KafkaTopic +metadata: + name: my-first-topic + labels: + strimzi.io/cluster: my-cluster +spec: + partitions: 3 + replicas: 1 + config: + retention.ms: "86400000" + segment.bytes: "1073741824" diff --git a/lessons/02-lesson-2/prep.sh b/lessons/02-lesson-2/prep.sh new file mode 100755 index 0000000..450e09b --- /dev/null +++ b/lessons/02-lesson-2/prep.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +set -euo pipefail + +CLUSTER_NAME="gitops-tutorial" +GITEA_USER="tutorial-user" +GITEA_PASSWORD="tutorial-password" +GITEA_REPO="streamshub-gitops" +GITEA_HOST_PORT=3001 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } +error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; } +b64decode() { echo "$1" | base64 -d 2>/dev/null || echo "$1" | base64 -D 2>/dev/null; } + +cleanup() { + if [[ -n "${WORK_DIR:-}" ]]; then + rm -rf "${WORK_DIR}" + fi +} + +# ─── Step 1: Validate infrastructure ────────────────────────────────────────── + +info "Validating tutorial infrastructure..." + +if ! kind get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then + error "KinD cluster '${CLUSTER_NAME}' is not running." + error "Please run the setup script first: ../00-setup/setup.sh" + exit 1 +fi + +if ! kubectl get deployment strimzi-cluster-operator -n strimzi-operator &>/dev/null; then + error "Strimzi operator not found in namespace 'strimzi-operator'." + error "Please run the setup script first: ../00-setup/setup.sh" + exit 1 +fi + +if ! curl -sf "http://localhost:${GITEA_HOST_PORT}/api/v1/version" >/dev/null 2>&1; then + error "Gitea is not reachable on localhost:${GITEA_HOST_PORT}." + error "Please run the setup script first: ../00-setup/setup.sh" + exit 1 +fi + +info "Infrastructure checks passed." + +# ─── Step 2: Clean up previous lesson state ─────────────────────────────────── + +info "Cleaning up previous lesson state..." + +# Remove the lesson-1 ArgoCD Application (no cascade finalizer, so this is instant). +kubectl delete application kafka-tutorial -n argocd --ignore-not-found 2>/dev/null || true + +# Also remove any lesson-2 Applications from a previous run of this script. +kubectl delete application kafka-staging kafka-production -n argocd --ignore-not-found 2>/dev/null || true + +# Patch away Strimzi finalizers so the namespace deletion doesn't hang. +for cr_type in kafka kafkanodepool kafkatopic; do + if kubectl get "${cr_type}" -n kafka-tutorial &>/dev/null 2>&1; then + kubectl get "${cr_type}" -n kafka-tutorial -o name 2>/dev/null | \ + xargs -r -I{} kubectl patch {} -n kafka-tutorial \ + --type=merge -p '{"metadata":{"finalizers":null}}' 2>/dev/null || true + fi +done + +kubectl delete namespace kafka-tutorial --ignore-not-found +kubectl delete namespace kafka-staging --ignore-not-found +kubectl delete namespace kafka-production --ignore-not-found + +info "Previous lesson state cleaned up." + +# ─── Step 3: Configure Strimzi to watch staging and production namespaces ────── + +info "Configuring Strimzi operator for staging and production namespaces..." + +for ns in kafka-staging kafka-production; do + kubectl create namespace "${ns}" 2>/dev/null || true + for rb_name in strimzi-cluster-operator strimzi-cluster-operator-entity-operator-delegation strimzi-cluster-operator-watched; do + ROLE_REF=$(kubectl get rolebinding "${rb_name}" -n strimzi-operator -o jsonpath='{.roleRef.name}' 2>/dev/null || echo "") + if [[ -n "${ROLE_REF}" ]]; then + kubectl create rolebinding "${rb_name}" \ + --namespace "${ns}" \ + --clusterrole="${ROLE_REF}" \ + --serviceaccount=strimzi-operator:strimzi-cluster-operator 2>/dev/null || true + fi + done +done + +kubectl set env deployment/strimzi-cluster-operator -n strimzi-operator \ + STRIMZI_NAMESPACE='kafka-staging,kafka-production' + +info "Waiting for Strimzi operator to restart..." +kubectl rollout status deployment/strimzi-cluster-operator -n strimzi-operator --timeout=120s + +# ─── Step 4: Seed the Gitea repository with the overlay structure ───────────── + +info "Resetting Gitea repository to lesson-2 starting state..." + +WORK_DIR=$(mktemp -d) +trap cleanup EXIT + +git clone "http://${GITEA_USER}:${GITEA_PASSWORD}@localhost:${GITEA_HOST_PORT}/${GITEA_USER}/${GITEA_REPO}.git" "${WORK_DIR}/repo" 2>/dev/null + +rm -rf "${WORK_DIR}/repo/manifests" +mkdir -p "${WORK_DIR}/repo/manifests" +cp -r "${SCRIPT_DIR}/lesson-manifests/." "${WORK_DIR}/repo/manifests/" + +pushd "${WORK_DIR}/repo" >/dev/null +git add . +if git diff --cached --quiet; then + info "Gitea repo is already in lesson-2 starting state, skipping commit." +else + git -c user.name="Tutorial Setup" -c user.email="setup@tutorial.local" commit -m "Reset to lesson-2 starting state" + git push + info "Lesson-2 starting state pushed to Gitea." +fi +TARGET_REVISION=$(git rev-parse HEAD) +popd >/dev/null + +# ─── Step 5: Create ArgoCD Applications for staging and production ───────────── + +info "Creating ArgoCD applications for staging and production..." + +kubectl apply -f - </dev/null 2>&1 || true +kubectl annotate application kafka-production -n argocd argocd.argoproj.io/refresh=normal --overwrite >/dev/null 2>&1 || true + +for app in kafka-staging kafka-production; do + SYNC_STATUS="Unknown" + for i in $(seq 1 24); do + CURRENT_REVISION=$(kubectl get application "${app}" -n argocd -o jsonpath='{.status.sync.revision}' 2>/dev/null || echo "") + SYNC_STATUS=$(kubectl get application "${app}" -n argocd -o jsonpath='{.status.sync.status}' 2>/dev/null || echo "Unknown") + if [[ "${CURRENT_REVISION}" == "${TARGET_REVISION}" && "${SYNC_STATUS}" == "Synced" ]]; then + break + fi + sleep 5 + done + + if [[ "${SYNC_STATUS}" != "Synced" ]]; then + warn "Application '${app}' has not synced yet (status: ${SYNC_STATUS})." + warn "Check with: kubectl get application ${app} -n argocd" + else + info "Application '${app}' is synced." + fi +done + +info "Waiting for Kafka clusters to be ready (this may take a few minutes)..." +kubectl wait kafka/my-cluster --for=condition=Ready -n kafka-staging --timeout=600s 2>/dev/null & +kubectl wait kafka/my-cluster --for=condition=Ready -n kafka-production --timeout=600s 2>/dev/null & +wait + +# ─── Step 7: Print starting instructions ────────────────────────────────────── + +ARGOCD_PASSWORD=$(b64decode "$(kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}')") + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +info "Lesson 2 is ready. Open README.md and follow the lesson steps." +echo "" +echo " Clone the repo to follow along:" +echo " git clone http://${GITEA_USER}:${GITEA_PASSWORD}@localhost:${GITEA_HOST_PORT}/${GITEA_USER}/${GITEA_REPO}.git /tmp/gitops-lesson-2" +echo "" +echo " Gitea (your Git server): http://localhost:${GITEA_HOST_PORT}" +echo " Username: ${GITEA_USER} Password: ${GITEA_PASSWORD}" +echo "" +echo " ArgoCD Dashboard (open in a separate terminal):" +echo " kubectl port-forward svc/argocd-server -n argocd 8080:443" +echo " URL: https://localhost:8080" +echo " Username: admin" +echo " Password: ${ARGOCD_PASSWORD}" +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/lessons/README.md b/lessons/README.md index 028ce98..af5b5af 100644 --- a/lessons/README.md +++ b/lessons/README.md @@ -16,6 +16,7 @@ A shared cluster runs for the whole series. You set it up once, then run a short |---|-------|-----------------| | [Getting Started](00-setup/README.md) | Environment setup | Install the shared cluster, ArgoCD, Strimzi, and a local Git server. Run this once before any lesson. | | [Lesson 1](01-lesson-1/README.md) | Your First GitOps Change | Add a Kafka topic by editing a file in Git and watch ArgoCD deploy it automatically — without running `kubectl apply`. | +| [Lesson 2](02-lesson-2/README.md) | Promoting Changes Across Environments | Use kustomize overlays to manage staging and production separately, then promote a topic from staging to production with a single Git change. | --- From f298db4718087eda51f7b2b1073800bbe66efe3c Mon Sep 17 00:00:00 2001 From: Alex Creasy Date: Wed, 29 Jul 2026 17:32:37 +0100 Subject: [PATCH 2/2] Fix issue where teardown hangs Signed-off-by: Alex Creasy --- lessons/02-lesson-2/prep.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lessons/02-lesson-2/prep.sh b/lessons/02-lesson-2/prep.sh index 450e09b..9804bc5 100755 --- a/lessons/02-lesson-2/prep.sh +++ b/lessons/02-lesson-2/prep.sh @@ -53,18 +53,16 @@ kubectl delete application kafka-tutorial -n argocd --ignore-not-found 2>/dev/nu # Also remove any lesson-2 Applications from a previous run of this script. kubectl delete application kafka-staging kafka-production -n argocd --ignore-not-found 2>/dev/null || true -# Patch away Strimzi finalizers so the namespace deletion doesn't hang. -for cr_type in kafka kafkanodepool kafkatopic; do - if kubectl get "${cr_type}" -n kafka-tutorial &>/dev/null 2>&1; then - kubectl get "${cr_type}" -n kafka-tutorial -o name 2>/dev/null | \ - xargs -r -I{} kubectl patch {} -n kafka-tutorial \ +# Patch away Strimzi finalizers in all tutorial namespaces so deletions don't hang. +for ns in kafka-tutorial kafka-staging kafka-production; do + for cr_type in kafka kafkanodepool kafkatopic; do + kubectl get "${cr_type}" -n "${ns}" -o name 2>/dev/null | \ + xargs -r -I{} kubectl patch {} -n "${ns}" \ --type=merge -p '{"metadata":{"finalizers":null}}' 2>/dev/null || true - fi + done done -kubectl delete namespace kafka-tutorial --ignore-not-found -kubectl delete namespace kafka-staging --ignore-not-found -kubectl delete namespace kafka-production --ignore-not-found +kubectl delete namespace kafka-tutorial kafka-staging kafka-production --ignore-not-found info "Previous lesson state cleaned up."