Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
444 changes: 444 additions & 0 deletions lessons/02-lesson-2/README.md

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions lessons/02-lesson-2/lesson-manifests/base/combined-pool.yaml
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions lessons/02-lesson-2/lesson-manifests/base/kafka.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lessons/02-lesson-2/lesson-manifests/base/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- combined-pool.yaml
- kafka.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kafka-production
resources:
- ../../base
- namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: kafka-production
labels:
name: kafka-production
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kafka-staging
resources:
- ../../base
- namespace.yaml
- topic.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: kafka-staging
labels:
name: kafka-staging
12 changes: 12 additions & 0 deletions lessons/02-lesson-2/lesson-manifests/overlays/staging/topic.yaml
Original file line number Diff line number Diff line change
@@ -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"
225 changes: 225 additions & 0 deletions lessons/02-lesson-2/prep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#!/usr/bin/env bash
set -euo pipefail

CLUSTER_NAME="gitops-tutorial"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said before lets put all these constants in a common script

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..."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be function in a common module


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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The application name and namespaces in these commands should be variables, again probably in a common script file


# 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 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
done
done

kubectl delete namespace kafka-tutorial kafka-staging 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'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just have strimzi watch all namespaces cluster-wide?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have strimzi watch all namespaces cluster wide when these are the only namespaces the tutorial will use?

Not being facetious, genuinely curious what that achieves?


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 - <<EOF
apiVersion: argoproj.io/v1alpha1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why both apps are defined inside shell script?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll move them out into separate yaml files.

thanks!

kind: Application
metadata:
name: kafka-staging
namespace: argocd
spec:
project: default
source:
repoURL: http://gitea-http.gitea.svc:3000/tutorial-user/streamshub-gitops.git
targetRevision: main
path: manifests/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: kafka-staging
syncPolicy:
automated:
prune: true
selfHeal: true
managedNamespaceMetadata:
labels:
argocd.argoproj.io/managed-by: argocd
syncOptions:
- CreateNamespace=true
EOF

kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kafka-production
namespace: argocd
spec:
project: default
source:
repoURL: http://gitea-http.gitea.svc:3000/tutorial-user/streamshub-gitops.git
targetRevision: main
path: manifests/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: kafka-production
syncPolicy:
automated:
prune: true
selfHeal: true
managedNamespaceMetadata:
labels:
argocd.argoproj.io/managed-by: argocd
syncOptions:
- CreateNamespace=true
EOF

# ─── Step 6: Wait for both applications to sync ───────────────────────────────

info "Waiting for ArgoCD to sync both applications..."

kubectl annotate application kafka-staging -n argocd argocd.argoproj.io/refresh=normal --overwrite >/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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1 change: 1 addition & 0 deletions lessons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

---

Expand Down