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
13 changes: 13 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Goal
<!-- What does this PR accomplish? 1 sentence. -->

## Changes
-

## Testing
<!-- How did you verify it? -->

## Checklist
- [ ] Title is a clear sentence (≤ 70 chars)
- [ ] Commits are signed (`git log --show-signature`)
- [ ] `submissions/labN.md` updated
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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)
data/
app/data/
27 changes: 27 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-24.04"
config.vm.hostname = "quicknotes-vm"

config.vm.network "forwarded_port",
guest: 8080,
host: 18080,
host_ip: "127.0.0.1"

config.vm.synced_folder "./app", "/opt/quicknotes"

config.vm.provider "virtualbox" do |vb|
vb.cpus = 2
vb.memory = 1024
end

config.vm.provision "shell", inline: <<-SHELL
set -eux
GO_VERSION=1.24.6
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tgz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tgz
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
chmod +x /etc/profile.d/go.sh
/usr/local/go/bin/go version
SHELL
end
8 changes: 8 additions & 0 deletions ansible/ansible-pull.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=Converge this host from Git via ansible-pull
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/ansible-pull -U https://github.com/HNS2112/DevOps-Intro.git -C feature/lab7 -i ansible/inventory-local.ini ansible/playbook.yaml
10 changes: 10 additions & 0 deletions ansible/ansible-pull.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Run ansible-pull every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit=ansible-pull.service

[Install]
WantedBy=timers.target
5 changes: 5 additions & 0 deletions ansible/ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[defaults]
inventory = inventory.ini
deprecation_warnings = False
host_key_checking = False
stdout_callback = default
Binary file added ansible/files/quicknotes
Binary file not shown.
2 changes: 2 additions & 0 deletions ansible/inventory-local.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[quicknotes]
localhost ansible_connection=local
7 changes: 7 additions & 0 deletions ansible/inventory.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[quicknotes]
lab5-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant

[quicknotes:vars]
ansible_ssh_private_key_file=/home/hns/dev/DevOps-Intro/.vagrant/machines/default/virtualbox/private_key
ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
ansible_python_interpreter=/usr/bin/python3
61 changes: 61 additions & 0 deletions ansible/playbook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
- name: Deploy QuickNotes to the Lab 5 VM
hosts: quicknotes
become: true
gather_facts: false
vars:
qn_user: quicknotes
qn_data_dir: /var/lib/quicknotes
qn_binary: /usr/local/bin/quicknotes
listen_addr: ":8080"
data_path: "{{ qn_data_dir }}/notes.json"
seed_path: "{{ qn_data_dir }}/seed.json"
tasks:
- name: Create the quicknotes system user
ansible.builtin.user:
name: "{{ qn_user }}"
system: true
shell: /usr/sbin/nologin
create_home: false
- name: Ensure the data directory exists
ansible.builtin.file:
path: "{{ qn_data_dir }}"
state: directory
owner: "{{ qn_user }}"
group: "{{ qn_user }}"
mode: "0750"
- name: Copy the QuickNotes binary
ansible.builtin.copy:
src: files/quicknotes
dest: "{{ qn_binary }}"
mode: "0755"
owner: root
group: root
notify: restart quicknotes
- name: Copy the seed file
ansible.builtin.copy:
src: ../app/seed.json
dest: "{{ seed_path }}"
owner: "{{ qn_user }}"
group: "{{ qn_user }}"
mode: "0640"
- name: Render the systemd unit
ansible.builtin.template:
src: quicknotes.service.j2
dest: /etc/systemd/system/quicknotes.service
owner: root
group: root
mode: "0644"
notify: restart quicknotes
- name: Enable and start the service
ansible.builtin.systemd_service:
name: quicknotes
enabled: true
state: started
daemon_reload: true
handlers:
- name: restart quicknotes
ansible.builtin.systemd_service:
name: quicknotes
state: restarted
daemon_reload: true
19 changes: 19 additions & 0 deletions ansible/templates/quicknotes.service.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Unit]
Description=QuickNotes HTTP service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User={{ qn_user }}
Group={{ qn_user }}
WorkingDirectory={{ data_path | dirname }}
ExecStart={{ qn_binary }}
Environment=ADDR={{ listen_addr }}
Environment=DATA_PATH={{ data_path }}
Environment=SEED_PATH={{ seed_path }}
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
24 changes: 24 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# syntax=docker/dockerfile:1

FROM golang:1.24.6-bookworm AS builder
WORKDIR /src
COPY go.mod go.su[m] ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build \
-trimpath \
-ldflags='-s -w' \
-o /out/quicknotes .
RUN mkdir -p /data-empty

FROM busybox:1.37-uclibc AS busybox

FROM gcr.io/distroless/static:nonroot
WORKDIR /app
COPY --from=builder /out/quicknotes /app/quicknotes
COPY --from=builder /src/seed.json /app/seed.json
COPY --from=busybox /bin/wget /bin/wget
COPY --from=builder --chown=65532:65532 /data-empty /data
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/quicknotes"]
3 changes: 3 additions & 0 deletions app/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func (sw *statusWriter) WriteHeader(code int) {
func (s *Server) wrap(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: w, code: 200}
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
w.Header().Set("Cache-Control", "no-store")
h(sw, r)
s.requestsTotal.Add(1)
if c, ok := s.requestsByCode[sw.code]; ok {
Expand Down
1 change: 0 additions & 1 deletion app/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,3 @@ func TestMetrics_ExposesPrometheusFormat(t *testing.T) {
}
}
}

8 changes: 8 additions & 0 deletions evidence/lab3-failed-run.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
test Run unit tests 2026-08-04T17:34:27.6294100Z shell: /usr/bin/bash -e {0}
test Run unit tests 2026-08-04T17:34:27.6294414Z ##[endgroup]
test Run unit tests 2026-08-04T17:34:48.9027010Z --- FAIL: TestHealth_ReportsCount (0.00s)
test Run unit tests 2026-08-04T17:34:48.9027918Z handlers_test.go:53: notes count: 1
test Run unit tests 2026-08-04T17:34:48.9028377Z FAIL
test Run unit tests 2026-08-04T17:34:48.9028684Z FAIL quicknotes 0.015s
test Run unit tests 2026-08-04T17:34:48.9029011Z FAIL
test Run unit tests 2026-08-04T17:34:48.9618493Z ##[error]Process completed with exit code 1.
1 change: 1 addition & 0 deletions evidence/lab3-protection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"checks":[{"app_id":15368,"context":"vet"},{"app_id":15368,"context":"test"},{"app_id":15368,"context":"lint"}],"contexts":["vet","test","lint"],"contexts_url":"https://api.github.com/repos/HNS2112/DevOps-Intro/branches/main/protection/required_status_checks/contexts","strict":true,"url":"https://api.github.com/repos/HNS2112/DevOps-Intro/branches/main/protection/required_status_checks"}
27 changes: 27 additions & 0 deletions evidence/lab7/01-first-run.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

PLAY [Deploy QuickNotes to the Lab 5 VM] ***************************************

TASK [Create the quicknotes system user] ***************************************
changed: [lab5-vm]

TASK [Ensure the data directory exists] ****************************************
changed: [lab5-vm]

TASK [Copy the QuickNotes binary] **********************************************
changed: [lab5-vm]

TASK [Copy the seed file] ******************************************************
changed: [lab5-vm]

TASK [Render the systemd unit] *************************************************
changed: [lab5-vm]

TASK [Enable and start the service] ********************************************
changed: [lab5-vm]

RUNNING HANDLER [restart quicknotes] *******************************************
changed: [lab5-vm]

PLAY RECAP *********************************************************************
lab5-vm : ok=7 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

24 changes: 24 additions & 0 deletions evidence/lab7/02-idempotent.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

PLAY [Deploy QuickNotes to the Lab 5 VM] ***************************************

TASK [Create the quicknotes system user] ***************************************
ok: [lab5-vm]

TASK [Ensure the data directory exists] ****************************************
ok: [lab5-vm]

TASK [Copy the QuickNotes binary] **********************************************
ok: [lab5-vm]

TASK [Copy the seed file] ******************************************************
ok: [lab5-vm]

TASK [Render the systemd unit] *************************************************
ok: [lab5-vm]

TASK [Enable and start the service] ********************************************
ok: [lab5-vm]

PLAY RECAP *********************************************************************
lab5-vm : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

27 changes: 27 additions & 0 deletions evidence/lab7/03-selective-change.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

PLAY [Deploy QuickNotes to the Lab 5 VM] ***************************************

TASK [Create the quicknotes system user] ***************************************
ok: [lab5-vm]

TASK [Ensure the data directory exists] ****************************************
ok: [lab5-vm]

TASK [Copy the QuickNotes binary] **********************************************
ok: [lab5-vm]

TASK [Copy the seed file] ******************************************************
ok: [lab5-vm]

TASK [Render the systemd unit] *************************************************
changed: [lab5-vm]

TASK [Enable and start the service] ********************************************
ok: [lab5-vm]

RUNNING HANDLER [restart quicknotes] *******************************************
changed: [lab5-vm]

PLAY RECAP *********************************************************************
lab5-vm : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

39 changes: 39 additions & 0 deletions evidence/lab7/04-check-diff.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

PLAY [Deploy QuickNotes to the Lab 5 VM] ***************************************

TASK [Create the quicknotes system user] ***************************************
ok: [lab5-vm]

TASK [Ensure the data directory exists] ****************************************
ok: [lab5-vm]

TASK [Copy the QuickNotes binary] **********************************************
ok: [lab5-vm]

TASK [Copy the seed file] ******************************************************
ok: [lab5-vm]

TASK [Render the systemd unit] *************************************************
--- before: /etc/systemd/system/quicknotes.service
+++ after: /home/hns/.ansible/tmp/ansible-local-111168l9dvf9nu/tmpvar2epoe/quicknotes.service.j2
@@ -13,7 +13,7 @@
Environment=DATA_PATH=/var/lib/quicknotes/notes.json
Environment=SEED_PATH=/var/lib/quicknotes/seed.json
Restart=on-failure
-RestartSec=3
+RestartSec=5

[Install]
WantedBy=multi-user.target

changed: [lab5-vm]

TASK [Enable and start the service] ********************************************
ok: [lab5-vm]

RUNNING HANDLER [restart quicknotes] *******************************************
changed: [lab5-vm]

PLAY RECAP *********************************************************************
lab5-vm : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

28 changes: 28 additions & 0 deletions evidence/lab7/05-service-state.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
● quicknotes.service - QuickNotes HTTP service
Loaded: loaded (/etc/systemd/system/quicknotes.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-08-11 15:50:14 UTC; 22s ago
Main PID: 3128 (quicknotes)
Tasks: 7 (limit: 1056)
Memory: 1.4M (peak: 1.7M)
CPU: 2ms
CGroup: /system.slice/quicknotes.service
---
[Unit]
Description=QuickNotes HTTP service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=quicknotes
Group=quicknotes
WorkingDirectory=/var/lib/quicknotes
ExecStart=/usr/local/bin/quicknotes
Environment=ADDR=:8080
Environment=DATA_PATH=/var/lib/quicknotes/notes.json
Environment=SEED_PATH=/var/lib/quicknotes/seed.json
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
27 changes: 27 additions & 0 deletions evidence/lab7/06-ansible-pull.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
=== Timer installed and active ===
NEXT   LEFT LAST   PASSED UNIT  ACTIVATES 
Tue 2026-08-11 16:08:25 UTC 1min 52s Tue 2026-08-11 16:03:25 UTC 3min 7s ago ansible-pull.timer ansible-pull.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

=== Convergence timeline (UTC) ===
commit pushed 2026-08-11 16:00:11
timer fired 2026-08-11 16:03:18
service restarted 2026-08-11 16:03:36
verified from host 2026-08-11 16:05:15
elapsed: 3m 25s from push to applied state, no host command involved

=== Unit on the VM after convergence ===
Environment=ADDR=:8081

=== ansible-pull recap for that run ===
Aug 11 15:58:31 quicknotes-vm ansible-pull[3967]: localhost : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Aug 11 16:03:37 quicknotes-vm ansible-pull[4476]: TASK [Render the systemd unit] *************************************************
Aug 11 16:03:37 quicknotes-vm ansible-pull[4476]: PLAY RECAP *********************************************************************
Aug 11 16:03:37 quicknotes-vm ansible-pull[4476]: localhost : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

=== Note ===
The unit above still shows ADDR=:8081 because the snapshot was taken before the
timer picked up the revert commit. The next fire reconciled it back to :8080
with no host command — the loop works in both directions.
Loading