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
100 changes: 100 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: quicknotes-ci

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

permissions:
contents: read

defaults:
run:
working-directory: app

jobs:
vet:
name: vet (go ${{ matrix.go-version }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go-version:
- "1.23"
- "1.24"
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
cache: true
cache-dependency-path: app/go.mod
- name: Run go vet
run: go vet ./...

test:
name: test (go ${{ matrix.go-version }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go-version:
- "1.23"
- "1.24"
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
cache: true
cache-dependency-path: app/go.mod
- name: Run tests with race detector
run: go test -race -count=1 ./...

lint:
name: lint
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: "1.24"
cache: true
cache-dependency-path: app/go.mod
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
- name: Run golangci-lint
run: golangci-lint run

govulncheck:
name: govulncheck
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: "1.24"
cache: true
cache-dependency-path: app/go.mod
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- name: Run govulncheck
working-directory: app
run: govulncheck ./...
47 changes: 47 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: quicknotes-release

on:
push:
tags:
- "v*"

permissions:
contents: read
packages: write

env:
IMAGE_NAME: ghcr.io/ilyapechersky/devops-intro/quicknotes

jobs:
release:
name: build and push image
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

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

- name: Docker metadata
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ./app
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ github.ref_name }}
${{ env.IMAGE_NAME }}:latest
labels: ${{ steps.meta.outputs.labels }}
53 changes: 53 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# syntax=docker/dockerfile:1

FROM golang:1.24-alpine AS builder

WORKDIR /src

COPY go.mod ./
COPY go.sum* ./
RUN go mod download

COPY . .

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

RUN mkdir -p /tmp/healthcheck && \
printf '%s\n' \
'package main' \
'' \
'import (' \
' "net/http"' \
' "os"' \
')' \
'' \
'func main() {' \
' resp, err := http.Get("http://127.0.0.1:8080/health")' \
' if err != nil || resp.StatusCode != http.StatusOK {' \
' os.Exit(1)' \
' }' \
'}' \
> /tmp/healthcheck/main.go && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-trimpath \
-ldflags='-s -w' \
-o /out/healthcheck /tmp/healthcheck/main.go

RUN mkdir -p /empty-data && chown 65532:65532 /empty-data

FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /out/quicknotes /quicknotes
COPY --from=builder /out/healthcheck /healthcheck
COPY seed.json /seed.json
COPY --from=builder --chown=65532:65532 /empty-data /data

USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/quicknotes"]

HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["/healthcheck"]
5 changes: 5 additions & 0 deletions app/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func NewServer(store *Store) *Server {

func (s *Server) Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /", s.wrap(s.handleRoot))
mux.HandleFunc("GET /health", s.wrap(s.handleHealth))
mux.HandleFunc("GET /metrics", s.wrap(s.handleMetrics))
mux.HandleFunc("GET /notes", s.wrap(s.handleListNotes))
Expand Down Expand Up @@ -58,6 +59,10 @@ func (s *Server) wrap(h http.HandlerFunc) http.HandlerFunc {
}
}

func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"service": "quicknotes"})
}

func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"status": "ok",
Expand Down
2 changes: 1 addition & 1 deletion app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func main() {
server := NewServer(store)
srv := &http.Server{
Addr: addr,
Handler: server.Routes(),
Handler: securityHeaders(server.Routes()),
ReadHeaderTimeout: 5 * time.Second,
}

Expand Down
18 changes: 18 additions & 0 deletions app/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import "net/http"

func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
h.Set("Referrer-Policy", "no-referrer")
h.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
h.Set("Cross-Origin-Opener-Policy", "same-origin")
h.Set("Cross-Origin-Resource-Policy", "same-origin")
h.Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}
30 changes: 30 additions & 0 deletions app/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestSecurityHeaders_OnAllRoutes(t *testing.T) {
srv := newTestServer(t)
handler := securityHeaders(srv.Routes())

for _, path := range []string{"/", "/health", "/notes", "/metrics"} {
t.Run(path, func(t *testing.T) {
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, path, nil)
handler.ServeHTTP(rec, req)

if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" {
t.Fatalf("%s: X-Content-Type-Options = %q, want nosniff", path, got)
}
if got := rec.Header().Get("X-Frame-Options"); got != "DENY" {
t.Fatalf("%s: X-Frame-Options = %q, want DENY", path, got)
}
if got := rec.Header().Get("Content-Security-Policy"); got == "" {
t.Fatalf("%s: Content-Security-Policy missing", path)
}
})
}
}
5 changes: 5 additions & 0 deletions cloud/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM ghcr.io/ilyapechersky/devops-intro/quicknotes:v0.1.1

ENV ADDR=0.0.0.0:8080
ENV DATA_PATH=/data/notes.json
ENV SEED_PATH=/seed.json
21 changes: 21 additions & 0 deletions cloud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: QuickNotes Lab 10
emoji: 📝
colorFrom: blue
colorTo: green
sdk: docker
app_port: 8080
pinned: false
---

# QuickNotes on Hugging Face Spaces

This Space runs the Lab 6 QuickNotes image published to GitHub Container Registry.

Public API endpoints:

- `GET /health`
- `GET /notes`
- `POST /notes`

Image source: `ghcr.io/ilyapechersky/devops-intro/quicknotes:v0.1.1`
25 changes: 25 additions & 0 deletions cloud/teardown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Lab 10 teardown

## Hugging Face Space

1. Open https://huggingface.co/spaces/IlyaPechersky/quicknotes-lab10/settings
2. Scroll to **Delete this Space**
3. Confirm deletion

## Cloudflare quick tunnel

Stop the local `cloudflared` process. Quick tunnel URLs are ephemeral and stop working when the process exits.

## GitHub Container Registry package

The `quicknotes` package can stay public for course review. To remove it later:

1. Open https://github.com/users/IlyaPechersky/packages/container/devops-intro%2Fquicknotes/settings
2. Delete the package version or the whole package if no longer needed

## Git tag

```bash
git push --delete origin v0.1.1
git tag -d v0.1.1
```
31 changes: 31 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
services:
quicknotes:
build:
context: ./app
dockerfile: Dockerfile
image: quicknotes:lab6
ports:
- "8080:8080"
environment:
ADDR: "0.0.0.0:8080"
DATA_PATH: "/data/notes.json"
SEED_PATH: "/seed.json"
volumes:
- quicknotes-data:/data
restart: unless-stopped
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "/healthcheck"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s

volumes:
quicknotes-data:
1 change: 1 addition & 0 deletions lab9-artifacts/govulncheck-green.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No vulnerabilities found.
Loading