diff --git a/.gitignore b/.gitignore index 1c0a1e94b..f9fff8fab 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ Thumbs.db # *.sbom.cdx.json, zap-*.html/json, trivy-*.txt (Lab 9 scan evidence) # flake.nix, flake.lock (Lab 11) # wasm/main.go, spin.toml, go.sum (Lab 12) + +.vagrant/ diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..8df5c6f3e --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,70 @@ +# Lab 5 - QuickNotes in a Vagrant VM + +Vagrant.configure("2") do |config| + # Public Ubuntu 24.04 LTS Vagrant box + config.vm.box = "bento/ubuntu-24.04" + + # Identifies the VM clearly + config.vm.hostname = "quicknotes-vm" + + # Host 127.0.0.1:18080 -> Guest 8080 + # Bound to localhost so it is not exposed publicly + config.vm.network "forwarded_port", + guest: 8080, + host: 18080, + host_ip: "127.0.0.1" + + # Sync only the app directory into the VM + config.vm.synced_folder "./app", "/opt/quicknotes/app", type: "virtualbox" + + # Resource limits: 2 vCPU and 1024 MB RAM + config.vm.provider "virtualbox" do |vb| + vb.name = "quicknotes-lab5" + vb.cpus = 2 + vb.memory = 1024 + end + + # Install Go 1.24.5, build QuickNotes, and run it as a systemd service + config.vm.provision "shell", inline: <<-SHELL + set -eux + + GO_VERSION="1.24.5" + + apt-get update + apt-get install -y curl ca-certificates build-essential + + if ! /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION}"; then + rm -rf /usr/local/go + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + fi + + ln -sf /usr/local/go/bin/go /usr/local/bin/go + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt + + cd /opt/quicknotes/app + /usr/local/go/bin/go mod download + /usr/local/go/bin/go build -o /opt/quicknotes/qn . + + cat >/etc/systemd/system/quicknotes.service <<'EOF' +[Unit] +Description=QuickNotes Go App +After=network.target + +[Service] +WorkingDirectory=/opt/quicknotes/app +ExecStart=/opt/quicknotes/qn +Environment=ADDR=:8080 +Restart=always +User=vagrant +Group=vagrant + +[Install] +WantedBy=multi-user.target +EOF + + systemctl daemon-reload + systemctl enable quicknotes + systemctl restart quicknotes + SHELL +end \ No newline at end of file diff --git a/labs/lab1.md b/labs/lab1.md index bb4e226d9..eb319e50f 100644 --- a/labs/lab1.md +++ b/labs/lab1.md @@ -91,9 +91,20 @@ git config --global commit.gpgsign true git config --global tag.gpgsign true ``` -Tell the platform your SSH key is a **signing key**: -- GitHub: Settings → SSH and GPG keys → **New SSH key**, key type **Signing Key** -- GitLab: Profile → SSH Keys → tick "Usage type: Authentication & signing" +Now register the key on the platform. GitHub treats **Authentication** and **Signing** as *separate* roles for the same key, so you add it under both: + +- **Authentication Key** — lets you `clone` / `fetch` / `push` over SSH (`git@github.com:…`). If you cloned over HTTPS, or have never seen `ssh -T git@github.com` greet you by name, you don't have one configured yet — add it now or the `upstream` SSH remote will fail in Lab 2. +- **Signing Key** — gives your commits the **Verified** badge. + +- 🐙 GitHub: Settings → SSH and GPG keys → **New SSH key** → add the **same** `~/.ssh/id_ed25519.pub` **twice**, once with Key type **Authentication Key** and once with **Signing Key**. +- 🦊 GitLab: Profile → SSH Keys → a single key with **Usage type: Authentication & signing** covers both. + +Confirm authentication works before moving on: + +```bash +ssh -T git@github.com +# expect: Hi YOUR_USERNAME! You've successfully authenticated... +``` ### 1.4: Make a Signed Commit @@ -303,7 +314,8 @@ In `submissions/lab1.md`: ## Common Pitfalls - 🪤 **PR template doesn't auto-populate** — make sure the template is on `main` *before* opening the PR -- 🪤 **Commits show "Unverified"** — the SSH key must be added as a *Signing Key* on GitHub (not just an authentication key) +- 🪤 **Commits show "Unverified"** — the key must also be added as a **Signing Key** on GitHub; an Authentication Key alone won't verify commits (they're separate roles — see §1.3) +- 🪤 **`git@github.com: Permission denied (publickey)` on clone/fetch/push** — the *reverse* gap: your key is registered for signing but not as an **Authentication Key**. Add it as Authentication too (§1.3) and confirm with `ssh -T git@github.com`. Quick unblock for the *public* upstream: `git remote set-url upstream https://github.com/inno-devops-labs/DevOps-Intro.git` - 🪤 **`git push` rejected on `main`** — that's the bonus rule working as designed; push to `feature/lab1` instead - 🪤 **`gpg.format=ssh` ignored** — confirm Git ≥ 2.34: `git --version` - 🪤 **Pushed to the wrong branch** — `git switch feature/lab1` before `git push` diff --git a/labs/lab2.md b/labs/lab2.md index fca7b3f22..aae9acfb3 100644 --- a/labs/lab2.md +++ b/labs/lab2.md @@ -223,6 +223,7 @@ git bisect reset ## Common Pitfalls +- 🪤 **`git@github.com: Permission denied (publickey)` on `git fetch upstream`** — *not* a remote-config bug (the error is at the SSH layer, before Git reads the repo). Your key isn't registered for **authentication** on GitHub — and a **Signing Key** (Lab 1) does *not* count for auth, they're separate roles. Add the same `~/.ssh/id_ed25519.pub` as an **Authentication Key** (Lab 1 §1.3), verify with `ssh -T git@github.com`, then re-run. To unblock right now, the public upstream fetches over HTTPS with no key: `git remote set-url upstream https://github.com/inno-devops-labs/DevOps-Intro.git` - 🪤 **`reset --hard` without committing first** — your *uncommitted* edits really *are* gone (reflog only saves committed work). Always check `git status` first - 🪤 **`tag -v` says "no signature"** — you used `git tag NAME` instead of `git tag -a -s NAME -m "..."` - 🪤 **Rebase conflicts** — resolve, then `git rebase --continue`. Never `git rebase --skip` unless you know what you're skipping diff --git a/labs/lab3.md b/labs/lab3.md index 9f0970b20..87344cfb3 100644 --- a/labs/lab3.md +++ b/labs/lab3.md @@ -160,6 +160,23 @@ Tips: - GitLab: `parallel:matrix:` - Set `fail-fast: false` (GH) or equivalent so a single bad cell doesn't cancel the others — you want to *see* which combo broke +> ⚠️ **The matrix renames your checks — update branch protection (1.6) or your PR blocks forever.** A matrixed `test` job reports as `test (1.23)` and `test (1.24)`; the old required check named `test` will sit at *"Expected — Waiting for status to be reported"* indefinitely, even though every real check is green. Two fixes: +> +> 1. **Quick:** in the branch-protection rule, replace `vet`/`test` with the matrixed names (`vet (1.23)`, `vet (1.24)`, `test (1.23)`, `test (1.24)`). +> 2. **Robust (recommended):** add one aggregation job and require *only* it — then the matrix can change freely without touching protection settings: +> +> ```yaml +> ci-ok: +> if: always() +> needs: [vet, test, lint] +> runs-on: ubuntu-24.04 +> steps: +> - run: | +> test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" +> ``` +> +> The `if: always()` matters — without it, a failed `needs` job *skips* `ci-ok`, and a skipped required check lets the PR through on some configurations. + ### 2.3: Skip docs-only changes Edit your trigger so the pipeline runs **only** when something in `app/` or your CI config itself changes. README edits should not burn 4 minutes of CI time. @@ -179,6 +196,8 @@ Capture wall-clock times from the CI UI for three scenarios: > 💡 To get a clean baseline, temporarily disable each optimization with a commit, take a screenshot of the run time, then restore. +> 🧪 **Expect the cache rows to be boring — that's the finding, not a failure.** QuickNotes has **zero third-party dependencies** (look at `app/go.mod` — no `require` block, no `go.sum`), so the module cache has nothing to store and total wall-clock barely moves with `cache: true` vs `cache: false`. Most of your 60–80 s is runner provisioning, checkout, and the Go toolchain download — none of which `setup-go`'s cache touches. Report what you measured and *explain why* (that's design question **f** in disguise). To see where caching *would* pay, compare the **per-step** durations (`setup-go`, `go test`) instead of job totals, and note which step a real dependency-heavy project would save on. + ### 2.5: Document In `submissions/lab3.md`: @@ -284,6 +303,8 @@ Answer in 4-6 sentences: - 🪤 **Forgot `working-directory` (or `cd app`) for Go commands** — Go modules live in `app/`, not the repo root; commands run from the root will fail with "no Go files" - 🪤 **`fail-fast: true` (the GH Actions default) in a matrix** — one fail cancels the others; you can't see *which* combo broke - 🪤 **Branch protection set on someone else's fork's `main`** — you can only protect *your* fork's `main`. The upstream course repo has its own protection +- 🪤 **PR stuck on "Expected — Waiting for status to be reported" after adding the matrix** — the matrix renamed `test` → `test (1.23)`/`test (1.24)`, but branch protection still requires the old `test` context, which will never report again. Update the required-check names or switch to the `ci-ok` aggregation job (see §2.2) +- 🪤 **"Caching didn't speed anything up"** — on a zero-dependency module that's the *correct* result, not a mistake (see §2.4); don't pad the timing table with numbers you didn't observe - 🪤 **`golangci-lint` version not pinned** — "latest" pulls a new release tomorrow that may flag your code with new rules. Pin `v2.5.0` exactly - 🪤 **GitLab CI: incorrect anchor syntax** (`<<: *name`) — GitLab is strict; use the in-platform CI Lint tool (`Project → CI/CD → Editor → Validate`) - 🪤 **Cache hits expire after 7 days of inactivity on GH** — that's expected; the cache key is what protects you against poisoning diff --git a/submissions/lab5.md b/submissions/lab5.md new file mode 100644 index 000000000..861c770de --- /dev/null +++ b/submissions/lab5.md @@ -0,0 +1,164 @@ +## Task 2 - Snapshots: Save, Break, Restore + +### Initial working state + +Before taking the snapshot, I confirmed that the VM was running and that QuickNotes worked correctly. + +Commands: + +```powershell +vagrant status +vagrant ssh -c "go version" +vagrant ssh -c "curl -s http://localhost:8080/health" +curl.exe -s http://localhost:18080/health +``` + +Output: + +```text +Current machine states: + +default running (virtualbox) + +The VM is running. +go version go1.24.5 linux/amd64 +{"notes":6,"status":"ok"} +{"notes":6,"status":"ok"} +``` + +This shows that the VM was running, Go 1.24.5 was installed inside the guest, QuickNotes responded inside the VM on port 8080, and the host could reach it through the forwarded port 18080. + +--- + +### Snapshot save + +I saved a clean working snapshot named `quicknotes-clean`. + +Command: + +```powershell +vagrant snapshot save quicknotes-clean +``` + +Output: + +```text +==> default: Snapshotting the machine as 'quicknotes-clean'... +==> default: Snapshot saved! You can restore the snapshot at any time by +==> default: using `vagrant snapshot restore`. You can delete it using +==> default: `vagrant snapshot delete`. +``` + +I then listed the snapshots to confirm it was created. + +Command: + +```powershell +vagrant snapshot list +``` + +Output: + +```text +==> default: +quicknotes-clean +``` + +--- + +### Break the VM deliberately + +To break the VM, I removed the Go installation and the Go binaries from the guest. + +Command: + +```powershell +vagrant ssh -c "sudo rm -rf /usr/local/go /usr/local/bin/go /usr/local/bin/gofmt" +``` + +I verified that the VM was broken by checking the Go version again. + +Command: + +```powershell +vagrant ssh -c "go version" +``` + +Output: + +```text +bash: line 1: go: command not found +``` + +This proves that the VM was deliberately broken because the Go toolchain was no longer available. + +--- + +### Restore from snapshot + +I restored the VM from the `quicknotes-clean` snapshot and measured the restore time using PowerShell's `Measure-Command`. + +Command: + +```powershell +$restoreTime = Measure-Command { vagrant snapshot restore quicknotes-clean } +$restoreTime +``` + +Output: + +```text +Days : 0 +Hours : 0 +Minutes : 0 +Seconds : 18 +Milliseconds : 74 +Ticks : 180741974 +TotalDays : 0.000209192099537037 +TotalHours : 0.00502061038888889 +TotalMinutes : 0.301236623333333 +TotalSeconds : 18.0741974 +TotalMilliseconds : 18074.1974 +``` + +The restore took approximately **18.07 seconds**. + +--- + +### Verify recovery + +After restoring the snapshot, I verified that Go was available again and that QuickNotes was still reachable both inside the VM and from the host. + +Commands: + +```powershell +vagrant ssh -c "go version" +vagrant ssh -c "curl -s http://localhost:8080/health" +curl.exe -s http://localhost:18080/health +``` + +Output: + +```text +go version go1.24.5 linux/amd64 +{"notes":6,"status":"ok"} +{"notes":6,"status":"ok"} +``` + +This confirms that restoring the snapshot successfully recovered the VM to the previous working state. + +--- + +### Design questions + +#### e) Snapshots are not backups + +Snapshots are not backups because they usually depend on the original VM disk and are often stored on the same host. If the host disk fails, the VM directory is deleted, or the snapshot chain becomes corrupted, the snapshot may be lost together with the VM. A real backup should be independent from the original machine and restorable somewhere else. + +#### f) Copy-on-write + +Copy-on-write means that taking a snapshot does not immediately create a full duplicate of the VM disk. Instead, VirtualBox keeps the original disk state and stores only the blocks that change after the snapshot. This means that 10 snapshots may initially use much less space than 10 full VM copies, but disk usage grows as more changes are made across the snapshot chain. + +#### g) When is snapshotting an antipattern? + +Snapshotting becomes an antipattern when it replaces proper automation, provisioning, or backups. Long chains of snapshots can become fragile, consume increasing disk space, and make the VM harder to reason about. For long-term infrastructure, it is better to rebuild the environment from code using tools such as Vagrant provisioning or Ansible rather than relying on a pile of old VM states.