Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
80e2c0b
docs: add PR template
sparrow12345 Jun 9, 2026
53fa0ff
docs: upstream moved while you worked
sparrow12345 Jun 9, 2026
ddda4f3
docs(lab1): start submission
sparrow12345 Jun 9, 2026
6426df9
docs(lab1): task 1 submission
sparrow12345 Jun 9, 2026
f8fe13f
docs(lab1): finish submission
sparrow12345 Jun 9, 2026
73af335
docs(lab1): final submission
sparrow12345 Jun 9, 2026
e2b0f10
wip(lab2): start
sparrow12345 Jun 9, 2026
27c939d
wip(lab2): more progress
sparrow12345 Jun 9, 2026
55b7fa5
docs(lab2): task 1 submission
sparrow12345 Jun 9, 2026
b96d835
docs(lab2): task 2 submission
sparrow12345 Jun 9, 2026
188a380
ci(lab3): add PR-gate
sparrow12345 Jun 15, 2026
c2cffd1
ci(lab3): deliberately break tests
sparrow12345 Jun 15, 2026
0eecae8
ci(lab3): fix broken test
sparrow12345 Jun 15, 2026
2bed2f2
ci(lab3): baseline run
sparrow12345 Jun 15, 2026
21c52c7
ci(lab3): cache run
sparrow12345 Jun 15, 2026
1d299ac
ci(lab3): matrix+cache run
sparrow12345 Jun 15, 2026
84743f6
ci(lab3): skip docs-only in ci
sparrow12345 Jun 15, 2026
6fa64e3
ci(lab3): lab3 submission
sparrow12345 Jun 15, 2026
a623f7a
ci(lab4): lab4 submission
sparrow12345 Jun 16, 2026
0ab7eca
virtualization(lab5): task 1 submission
sparrow12345 Jun 22, 2026
f992bed
virtualization(lab5): task 2 submission
sparrow12345 Jun 23, 2026
f1a5e61
Containers(lab6): lab6 submission
sparrow12345 Jun 23, 2026
cde1cd8
Ansible(lab7): lab7 submission
sparrow12345 Jun 29, 2026
7cc33d8
Monitoring(lab8): lab8 submission
sparrow12345 Jun 30, 2026
7b972bd
DevSecOps(lab9): lab9 submission
sparrow12345 Jul 6, 2026
39acad1
Cloud(lab10): add ghcr.io release workflow
sparrow12345 Jul 7, 2026
ad63ab4
Cloud(lab10): lab10 submission
sparrow12345 Jul 7, 2026
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
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- 'app/**'
- '.github/workflows/ci.yml'

permissions:
contents: read

defaults:
run:
working-directory: app

jobs:
vet:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go: ['1.23', '1.24']
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a
with:
go-version: ${{ matrix.go }}
cache-dependency-path: app/go.sum
- run: go vet ./...

test:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go: ['1.23', '1.24']
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a
with:
go-version: ${{ matrix.go }}
cache-dependency-path: app/go.sum
- run: go test -race -count=1 ./...

lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a
with:
go-version: '1.24'
cache-dependency-path: app/go.sum
- uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee
with:
version: v2.12.0
working-directory: app
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: read
packages: write

jobs:
build-push:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

- name: Set image name
id: img
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}/quicknotes" >> "$GITHUB_OUTPUT"

- name: Log in to ghcr.io
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
with:
images: ${{ steps.img.outputs.name }}
tags: |
type=semver,pattern={{version}}
type=raw,value=latest

- name: Set up Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c

- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with:
context: app
file: app/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
51 changes: 51 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
Vagrant.configure("2") do |config|

config.vm.box = "bento/ubuntu-24.04"

config.vm.hostname = "quicknotes"

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

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

config.vm.provider "virtualbox" do |vb|
vb.name = "quicknotes-lab5"
vb.cpus = 2
vb.memory = 1024
vb.customize ["modifyvm", :id, "--paravirtprovider", "legacy"]
end

config.vm.provision "shell", inline: <<-SHELL
if ! /usr/local/go/bin/go version 2>/dev/null | grep -q "go1.24.5"; then
curl -fsSL "https://go.dev/dl/go1.24.5.linux-amd64.tar.gz" -o /tmp/go.tgz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tgz
fi
export PATH=$PATH:/usr/local/go/bin
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
mkdir -p /opt/quicknotes/bin /var/lib/quicknotes
cd /opt/quicknotes/app
go build -o /opt/quicknotes/bin/qn .
cat > /etc/systemd/system/quicknotes.service <<'UNIT'
[Unit]
Description=QuickNotes
After=network.target

[Service]
WorkingDirectory=/opt/quicknotes/app
Environment=ADDR=:8080
Environment=DATA_PATH=/var/lib/quicknotes/notes.json
Environment=SEED_PATH=/opt/quicknotes/app/seed.json
ExecStart=/opt/quicknotes/bin/qn
Restart=on-failure

[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now quicknotes
sleep 2
systemctl --no-pager status quicknotes | head -n 5
SHELL
end
Binary file added ansible/files/quicknotes
Binary file not shown.
5 changes: 5 additions & 0 deletions ansible/inventory.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[lab5]
quicknotes ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=/home/moze/Documents/DevOps/DevOps-Intro/.vagrant/machines/default/virtualbox/private_key

[lab5:vars]
ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
62 changes: 62 additions & 0 deletions ansible/playbook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
- name: Deploy QuickNotes
hosts: lab5
become: true
gather_facts: false

vars:
app_user: quicknotes
data_dir: /var/lib/quicknotes
bin_path: /usr/local/bin/quicknotes
listen_addr: ":7070"

tasks:
- name: Create system user
ansible.builtin.user:
name: "{{ app_user }}"
system: true
shell: /usr/sbin/nologin
create_home: false

- name: Ensure data directory
ansible.builtin.file:
path: "{{ data_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"

- name: Copy QuickNotes binary
ansible.builtin.copy:
src: files/quicknotes
dest: "{{ bin_path }}"
mode: "0755"
notify: restart quicknotes

- name: Copy seed file
ansible.builtin.copy:
src: ../app/seed.json
dest: "{{ data_dir }}/seed.json"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"

- name: Render systemd unit
ansible.builtin.template:
src: templates/quicknotes.service.j2
dest: /etc/systemd/system/quicknotes.service
mode: "0644"
notify: restart quicknotes

- name: Enable and start service
ansible.builtin.systemd:
name: quicknotes
enabled: true
state: started
daemon_reload: true

handlers:
- name: restart quicknotes
ansible.builtin.systemd:
name: quicknotes
state: restarted
daemon_reload: true
17 changes: 17 additions & 0 deletions ansible/templates/quicknotes.service.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
Description=QuickNotes
After=network-online.target
Wants=network-online.target

[Service]
User={{ app_user }}
WorkingDirectory={{ data_dir }}
Environment=ADDR={{ listen_addr }}
Environment=DATA_PATH={{ data_dir }}/notes.json
Environment=SEED_PATH={{ data_dir }}/seed.json
ExecStart={{ bin_path }}
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
26 changes: 26 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Stage 1: builder stage
FROM golang:1.24 AS builder

WORKDIR /src

COPY go.mod ./
RUN go mod download

RUN mkdir -p /data

COPY . .

RUN CGO_ENABLED=0 GOOS=linux \
go build -trimpath -ldflags='-s -w' -o /quicknotes .

# Stage 2: runtime stage
FROM gcr.io/distroless/static:nonroot

USER 65532:65532

COPY --from=builder /quicknotes /quicknotes
COPY --from=builder --chown=65532:65532 /data /data

EXPOSE 8080

ENTRYPOINT ["/quicknotes"]
4 changes: 2 additions & 2 deletions app/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ func NewServer(store *Store) *Server {
return &Server{store: store, requestsByCode: by}
}

func (s *Server) Routes() *http.ServeMux {
func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", s.wrap(s.handleHealth))
mux.HandleFunc("GET /metrics", s.wrap(s.handleMetrics))
mux.HandleFunc("GET /notes", s.wrap(s.handleListNotes))
mux.HandleFunc("POST /notes", s.wrap(s.handleCreateNote))
mux.HandleFunc("GET /notes/{id}", s.wrap(s.handleGetNote))
mux.HandleFunc("DELETE /notes/{id}", s.wrap(s.handleDeleteNote))
return mux
return securityHeaders(mux)
}

type statusWriter struct {
Expand Down
10 changes: 10 additions & 0 deletions app/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ func TestDeleteNote_RemovesAndReturns204(t *testing.T) {
}
}

func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) {
srv := newTestServer(t)
for _, target := range []string{"/health", "/notes", "/metrics"} {
rec := do(t, srv, http.MethodGet, target, nil)
if got := rec.Header().Get("Cache-Control"); got != "no-store" {
t.Errorf("%s: Cache-Control = %q, want no-store", target, got)
}
}
}

func TestMetrics_ExposesPrometheusFormat(t *testing.T) {
srv := newTestServer(t)
_ = do(t, srv, http.MethodPost, "/notes", map[string]string{"title": "x"})
Expand Down
8 changes: 8 additions & 0 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ func main() {
dataPath := envOrDefault("DATA_PATH", "data/notes.json")
seedPath := envOrDefault("SEED_PATH", "seed.json")

if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
resp, err := http.Get("http://localhost:8080/health")
if err != nil || resp.StatusCode != http.StatusOK {
os.Exit(1)
}
os.Exit(0)
}

if err := ensureSeeded(dataPath, seedPath); err != nil {
log.Fatalf("seed: %v", err)
}
Expand Down
10 changes: 10 additions & 0 deletions app/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import "net/http"

func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
1 change: 1 addition & 0 deletions cloud/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM ghcr.io/sparrow12345/devops-intro/quicknotes:0.1.0
13 changes: 13 additions & 0 deletions cloud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: QuickNotes
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 8080
pinned: false
---

# QuickNotes on HF Spaces

Runs the QuickNotes container from `ghcr.io/sparrow12345/devops-intro/quicknotes`.
Endpoints: `/health`, `/notes`.
Loading