diff --git a/apps/ci/pipeline.yaml b/apps/ci/pipeline.yaml new file mode 100644 index 0000000..593b434 --- /dev/null +++ b/apps/ci/pipeline.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: workshop-pipeline + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "6" +spec: + project: workshop + source: + repoURL: https://github.com/innspire/ops-demo.git + targetRevision: HEAD + path: manifests/ci/pipeline + destination: + server: https://kubernetes.default.svc + namespace: tekton-pipelines + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/ci/tekton.yaml b/apps/ci/tekton.yaml new file mode 100644 index 0000000..d3358f4 --- /dev/null +++ b/apps/ci/tekton.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: tekton + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "5" +spec: + project: workshop + source: + repoURL: https://github.com/innspire/ops-demo.git + targetRevision: HEAD + path: manifests/ci/tekton + kustomize: {} + destination: + server: https://kubernetes.default.svc + namespace: tekton-pipelines + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/docs/04-tekton-pipeline.md b/docs/04-tekton-pipeline.md new file mode 100644 index 0000000..bccbda5 --- /dev/null +++ b/docs/04-tekton-pipeline.md @@ -0,0 +1,216 @@ +# Exercise 04 — Tekton Pipeline (GitOps Loop) + +**Time**: ~45 min +**Goal**: Build an automated pipeline that bumps the podinfo image tag in Git and watches ArgoCD roll out the new version — the full GitOps CI/CD loop. + +--- + +## What you'll learn +- Tekton concepts: Task, Pipeline, PipelineRun, Workspace +- How a pipeline commits to Git to trigger a GitOps deployment (no container registry needed) +- The full loop: pipeline push → ArgoCD detects → rolling update → new version in browser + +--- + +## The loop visualised + +``` +You trigger PipelineRun + │ + ▼ +Task 1: clone repo +Task 2: validate manifests (kubectl dry-run) +Task 3: bump image tag → deployment.yaml: 6.6.2 → 6.7.0 +Task 4: git commit + push + │ + ▼ +ArgoCD polls repo (or click Refresh) + │ + ▼ +ArgoCD syncs podinfo Deployment + │ + ▼ +Rolling update → podinfo v6.7.0 in your browser +``` + +--- + +## Prerequisites + +Exercises 01–03 complete. podinfo is reachable at **http://podinfo.192.168.56.200.nip.io** and shows version **6.6.2**. + +You need: +- A GitHub account with write access to the `ops-demo` repo +- A GitHub Personal Access Token (PAT) with **repo** scope + +--- + +## Steps + +### 1. Verify Tekton is installed + +The `apps/ci/tekton.yaml` and `apps/ci/pipeline.yaml` ArgoCD Applications are +already in the repo. ArgoCD is installing Tekton via a kustomize remote reference. + +Wait for the install to complete (~3–5 min after the app appears in ArgoCD): + +```bash +kubectl get pods -n tekton-pipelines +# NAME READY STATUS RESTARTS +# tekton-pipelines-controller-xxx 1/1 Running 0 +# tekton-pipelines-webhook-xxx 1/1 Running 0 +``` + +Also check that the pipeline resources are synced: + +```bash +kubectl get pipeline -n tekton-pipelines +# NAME AGE +# gitops-image-bump Xm +``` + +--- + +### 2. Set up Git credentials + +The pipeline needs to push a commit to GitHub. Create a Personal Access Token: + +1. Go to **GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens** (or classic with `repo` scope) +2. Give it write access to the `ops-demo` repository + +Then create the Kubernetes Secret (this command replaces the placeholder Secret if it already exists): + +```bash +./scripts/set-git-credentials.sh +``` + +Verify: + +```bash +kubectl get secret git-credentials -n tekton-pipelines +# NAME TYPE DATA AGE +# git-credentials Opaque 2 5s +``` + +--- + +### 3. Trigger the pipeline + +Apply the PipelineRun (this is the only `kubectl apply` you'll run in this exercise): + +```bash +kubectl apply -f manifests/ci/pipeline/pipelinerun.yaml +``` + +Watch it run: + +```bash +kubectl get pipelinerun -n tekton-pipelines -w +``` + +Or follow the logs with tkn (Tekton CLI, optional): + +```bash +# If tkn is installed: +tkn pipelinerun logs -f -n tekton-pipelines bump-podinfo-to-670 +``` + +Or follow individual TaskRun pods: + +```bash +kubectl get pods -n tekton-pipelines -w +# Once a pod appears, you can: +kubectl logs -n tekton-pipelines -c step-bump --follow +``` + +The PipelineRun should complete in ~2–3 minutes. + +--- + +### 4. Verify the commit + +```bash +# Inside the VM — check the latest commit on the remote +git fetch origin +git log origin/main --oneline -3 +# You should see something like: +# a1b2c3d chore(pipeline): bump podinfo to 6.7.0 +# ... +``` + +Or check GitHub directly in your browser. + +--- + +### 5. Watch ArgoCD sync + +In the ArgoCD UI, click **Refresh** on the **podinfo** application. + +ArgoCD will detect that `manifests/apps/podinfo/deployment.yaml` changed and +start a rolling update. + +```bash +kubectl rollout status deployment/podinfo -n podinfo +# deployment "podinfo" successfully rolled out +``` + +--- + +### 6. Confirm in the browser + +Open **http://podinfo.192.168.56.200.nip.io** — you should now see **version 6.7.0**. + +```bash +curl http://podinfo.192.168.56.200.nip.io | jq .version +# "6.7.0" +``` + +The full loop is complete. + +--- + +## Expected outcome + +``` +PipelineRun STATUS: Succeeded +deployment.yaml image tag: 6.7.0 +podinfo UI version: 6.7.0 +``` + +--- + +## Re-running the pipeline + +The `PipelineRun` name must be unique. To run again: + +```bash +# Option A: delete and re-apply with same name +kubectl delete pipelinerun bump-podinfo-to-670 -n tekton-pipelines +kubectl apply -f manifests/ci/pipeline/pipelinerun.yaml + +# Option B: create a new run with a different name +kubectl create -f manifests/ci/pipeline/pipelinerun.yaml +``` + +--- + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| PipelineRun stuck in "Running" forever | `kubectl describe pipelinerun -n tekton-pipelines bump-podinfo-to-670` | +| `git-credentials` Secret not found | Run `./scripts/set-git-credentials.sh` first | +| Push fails: 403 Forbidden | PAT has insufficient scope — needs `repo` write access | +| Push fails: remote already has this commit | Image tag already at 6.7.0; the pipeline is idempotent (nothing to push) | +| ArgoCD not syncing after push | Click **Refresh** in the UI; default poll interval is 3 min | +| Validate task fails | Check `kubectl apply --dry-run=client -f manifests/apps/podinfo/` manually | + +--- + +## What's next + +Exercise 05 is a quick wrap-up: you'll look at the full picture of what you built +and optionally trigger another upgrade cycle to cement the GitOps loop. + +If you have time, try the **Bonus Exercise 06**: deploy Prometheus + Grafana and +see cluster and podinfo metrics in a live dashboard. diff --git a/manifests/ci/pipeline/pipeline.yaml b/manifests/ci/pipeline/pipeline.yaml new file mode 100644 index 0000000..63eff39 --- /dev/null +++ b/manifests/ci/pipeline/pipeline.yaml @@ -0,0 +1,173 @@ +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: gitops-image-bump + namespace: tekton-pipelines +spec: + description: | + Validates manifests, bumps the podinfo image tag in deployment.yaml, + and pushes the commit back to the ops-demo repo. + ArgoCD then detects the change and rolls out the new image. + + params: + - name: repo-url + type: string + description: URL of the ops-demo git repository + default: https://github.com/innspire/ops-demo.git + - name: new-tag + type: string + description: New podinfo image tag to set (e.g. 6.7.0) + default: "6.7.0" + - name: git-user-name + type: string + description: Git author name for the bump commit + default: "Workshop Pipeline" + - name: git-user-email + type: string + description: Git author email for the bump commit + default: "pipeline@workshop.local" + + workspaces: + - name: source + description: Workspace for cloning the repo + - name: git-credentials + description: Secret with GitHub username + PAT (basic-auth) + + tasks: + # ── Task 1: Clone the repo ───────────────────────────────────────────── + - name: clone + taskSpec: + workspaces: + - name: source + - name: git-credentials + params: + - name: repo-url + - name: git-user-name + - name: git-user-email + steps: + - name: clone + image: alpine/git:latest + workingDir: /workspace/source + env: + - name: GIT_USERNAME + valueFrom: + secretKeyRef: + name: git-credentials + key: username + - name: GIT_PASSWORD + valueFrom: + secretKeyRef: + name: git-credentials + key: password + script: | + #!/bin/sh + set -eu + # Inject credentials into the clone URL + REPO=$(echo "$(params.repo-url)" | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|") + git clone "${REPO}" . + git config user.name "$(params.git-user-name)" + git config user.email "$(params.git-user-email)" + echo "Cloned $(git log --oneline -1)" + workspaces: + - name: source + workspace: source + - name: git-credentials + workspace: git-credentials + params: + - name: repo-url + value: $(params.repo-url) + - name: git-user-name + value: $(params.git-user-name) + - name: git-user-email + value: $(params.git-user-email) + + # ── Task 2: Validate manifests (dry-run) ────────────────────────────── + - name: validate + runAfter: [clone] + taskSpec: + workspaces: + - name: source + steps: + - name: dry-run + image: bitnami/kubectl:latest + workingDir: /workspace/source + script: | + #!/bin/sh + set -eu + echo "Running kubectl dry-run on manifests/apps/podinfo/" + kubectl apply --dry-run=client -f manifests/apps/podinfo/ + echo "Validation passed." + workspaces: + - name: source + workspace: source + + # ── Task 3: Bump image tag ───────────────────────────────────────────── + - name: bump-image-tag + runAfter: [validate] + taskSpec: + workspaces: + - name: source + params: + - name: new-tag + steps: + - name: bump + image: mikefarah/yq:4.44.3 + workingDir: /workspace/source + script: | + #!/bin/sh + set -eu + FILE="manifests/apps/podinfo/deployment.yaml" + CURRENT=$(yq '.spec.template.spec.containers[0].image' "${FILE}") + echo "Current image: ${CURRENT}" + yq -i '.spec.template.spec.containers[0].image = "ghcr.io/stefanprodan/podinfo:$(params.new-tag)"' "${FILE}" + UPDATED=$(yq '.spec.template.spec.containers[0].image' "${FILE}") + echo "Updated image: ${UPDATED}" + workspaces: + - name: source + workspace: source + params: + - name: new-tag + value: $(params.new-tag) + + # ── Task 4: Commit and push ──────────────────────────────────────────── + - name: git-commit-push + runAfter: [bump-image-tag] + taskSpec: + workspaces: + - name: source + - name: git-credentials + params: + - name: new-tag + steps: + - name: push + image: alpine/git:latest + workingDir: /workspace/source + env: + - name: GIT_USERNAME + valueFrom: + secretKeyRef: + name: git-credentials + key: username + - name: GIT_PASSWORD + valueFrom: + secretKeyRef: + name: git-credentials + key: password + script: | + #!/bin/sh + set -eu + git add manifests/apps/podinfo/deployment.yaml + git commit -m "chore(pipeline): bump podinfo to $(params.new-tag)" + + # Inject credentials for push + REMOTE_URL=$(git remote get-url origin | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|") + git push "${REMOTE_URL}" HEAD:main + echo "Pushed commit: $(git log --oneline -1)" + workspaces: + - name: source + workspace: source + - name: git-credentials + workspace: git-credentials + params: + - name: new-tag + value: $(params.new-tag) diff --git a/manifests/ci/pipeline/pipelinerun.yaml b/manifests/ci/pipeline/pipelinerun.yaml new file mode 100644 index 0000000..09eec4b --- /dev/null +++ b/manifests/ci/pipeline/pipelinerun.yaml @@ -0,0 +1,32 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + # Change the name (e.g. bump-to-670-run2) each time you trigger the pipeline, + # or delete the old PipelineRun first — names must be unique. + name: bump-podinfo-to-670 + namespace: tekton-pipelines +spec: + pipelineRef: + name: gitops-image-bump + taskRunTemplate: + serviceAccountName: pipeline-runner + params: + - name: repo-url + value: https://github.com/innspire/ops-demo.git + - name: new-tag + value: "6.7.0" + - name: git-user-name + value: "Workshop Pipeline" + - name: git-user-email + value: "pipeline@workshop.local" + workspaces: + - name: source + volumeClaimTemplate: + spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 1Gi + - name: git-credentials + secret: + secretName: git-credentials diff --git a/manifests/ci/pipeline/serviceaccount.yaml b/manifests/ci/pipeline/serviceaccount.yaml new file mode 100644 index 0000000..0007483 --- /dev/null +++ b/manifests/ci/pipeline/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: pipeline-runner + namespace: tekton-pipelines +# The git-credentials Secret is NOT in this repo (it contains a real GitHub token). +# Create it before running the pipeline: +# ./scripts/set-git-credentials.sh diff --git a/manifests/ci/tekton/kustomization.yaml b/manifests/ci/tekton/kustomization.yaml new file mode 100644 index 0000000..d1192d3 --- /dev/null +++ b/manifests/ci/tekton/kustomization.yaml @@ -0,0 +1,6 @@ +# Installs Tekton Pipelines v0.65.1 via kustomize remote reference. +# ArgoCD applies this with its built-in kustomize support. +# Images are pre-pulled by the Vagrantfile, so this only needs network to +# fetch the manifest once (not the images). +resources: + - https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.65.1/release.yaml diff --git a/scripts/set-git-credentials.sh b/scripts/set-git-credentials.sh new file mode 100755 index 0000000..9c6a786 --- /dev/null +++ b/scripts/set-git-credentials.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# set-git-credentials.sh — Create the git-credentials Secret for the Tekton pipeline. +# +# Usage: +# ./scripts/set-git-credentials.sh +# +# The PAT needs: repo (read + write) scope. +# The Secret is NOT stored in git — it lives only in the cluster. +# +# Run this once before triggering the PipelineRun. + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " + exit 1 +fi + +GITHUB_USER="$1" +GITHUB_PAT="$2" +NAMESPACE="tekton-pipelines" + +echo "→ Creating git-credentials Secret in namespace ${NAMESPACE}" + +kubectl create secret generic git-credentials \ + --namespace "${NAMESPACE}" \ + --from-literal=username="${GITHUB_USER}" \ + --from-literal=password="${GITHUB_PAT}" \ + --dry-run=client -o yaml | kubectl apply -f - + +echo "✓ Secret created. The pipeline is ready to run." +echo "" +echo " Trigger the pipeline:" +echo " kubectl apply -f manifests/ci/pipeline/pipelinerun.yaml" +echo "" +echo " Watch progress:" +echo " kubectl get pipelinerun -n tekton-pipelines -w" +echo " # or use: tkn pipelinerun logs -f -n tekton-pipelines bump-podinfo-to-670"