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

on:
push:
tags:
- "v*"

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read
packages: write

jobs:
publish:
name: Build and push QuickNotes image
runs-on: ubuntu-24.04
timeout-minutes: 20

steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2, resolved 2026-07-07

- name: Compute image tags
id: meta
shell: bash
run: |
set -euo pipefail
image="ghcr.io/${GITHUB_REPOSITORY}/quicknotes"
image="$(printf '%s' "$image" | tr '[:upper:]' '[:lower:]')"
version="${GITHUB_REF_NAME}"

{
echo "image=${image}"
echo "version=${version}"
echo "version_tag=${image}:${version}"
echo "latest_tag=${image}:latest"
} >> "$GITHUB_OUTPUT"

printf 'Publishing %s and %s\n' "${image}:${version}" "${image}:latest"

- name: Login to GitHub Container Registry
shell: bash
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
printf '%s' "$GHCR_TOKEN" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin

- name: Build and push image
shell: bash
run: |
set -euo pipefail
docker buildx create --use --name quicknotes-release-builder || docker buildx use quicknotes-release-builder
docker buildx inspect --bootstrap
docker buildx build \
--platform linux/amd64 \
--tag "${{ steps.meta.outputs.version_tag }}" \
--tag "${{ steps.meta.outputs.latest_tag }}" \
--push \
./app

- name: Print published image digest
shell: bash
run: |
set -euo pipefail
docker buildx imagetools inspect "${{ steps.meta.outputs.version_tag }}"
98 changes: 98 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# Lab 5 — QuickNotes in a Vagrant VM.
# Boots an Ubuntu 24.04 LTS VM, installs Go 1.24.x, builds QuickNotes from the
# synced ./app folder, and runs it as a systemd service on guest port 8080.
# The host reaches it on http://127.0.0.1:18080.

GO_VERSION = "1.24.5"

Vagrant.configure("2") do |config|
# Ubuntu 24.04 LTS. The bento box ships VirtualBox Guest Additions, which we
# need for the default (virtualbox) synced-folder type.
config.vm.box = "bento/ubuntu-24.04"

# Identify the VM clearly (shows up in the shell prompt and `vagrant ssh`).
config.vm.hostname = "quicknotes-vm"

# Forward host 18080 -> guest 8080, bound to loopback only so the app is not
# exposed to the local network.
config.vm.network "forwarded_port", guest: 8080, host: 18080, host_ip: "127.0.0.1"

# Mount the application source into the guest (default type: virtualbox).
config.vm.synced_folder "./app", "/opt/quicknotes/app"

# Cap resources at 2 vCPU / 1024 MB RAM (over-provisioning is an antipattern).
config.vm.provider "virtualbox" do |vb|
vb.name = "quicknotes-vm"
vb.memory = 1024
vb.cpus = 2
end

# Provisioning runs on first `vagrant up`; re-run with `vagrant provision`.
# The script is idempotent, so `vagrant up --provision` is safe to repeat.
config.vm.provision "shell", inline: <<-SHELL
#!/usr/bin/env bash
set -euo pipefail

GO_VERSION="#{GO_VERSION}"
case "$(uname -m)" in
x86_64)
GO_ARCH="amd64"
;;
aarch64|arm64)
GO_ARCH="arm64"
;;
*)
echo "Unsupported guest architecture: $(uname -m)" >&2
exit 1
;;
esac
GO_TARBALL="go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"

export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y curl

# Install the pinned Go version only if it is not already present.
if ! /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION} "; then
echo "Installing Go ${GO_VERSION}..."
curl -fsSL "https://go.dev/dl/${GO_TARBALL}" -o "/tmp/${GO_TARBALL}"
rm -rf /usr/local/go
tar -C /usr/local -xzf "/tmp/${GO_TARBALL}"
fi

# Make Go available on PATH for interactive logins (e.g. `vagrant ssh`).
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh

# Build QuickNotes from the synced source into a stable location.
install -d /var/lib/quicknotes
/usr/local/go/bin/go build -C /opt/quicknotes/app -o /usr/local/bin/quicknotes .

# Run QuickNotes as a service so it survives reboots and starts on boot.
cat > /etc/systemd/system/quicknotes.service <<'UNIT'
[Unit]
Description=QuickNotes API
After=network-online.target
Wants=network-online.target

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

[Install]
WantedBy=multi-user.target
UNIT

systemctl daemon-reload
systemctl enable quicknotes
systemctl restart quicknotes

echo "Provisioning done. QuickNotes should be live on guest :8080."
SHELL
end
Binary file added ansible/files/quicknotes
Binary file not shown.
2 changes: 2 additions & 0 deletions ansible/inventory.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[quicknotes_vm]
quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=.vagrant/machines/default/virtualbox/private_key ansible_python_interpreter=/usr/bin/python3 ansible_ssh_common_args='-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa'
168 changes: 168 additions & 0 deletions ansible/playbook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
- name: Deploy QuickNotes to the Lab 5 VM
hosts: quicknotes_vm
become: true
gather_facts: false

vars:
quicknotes_user: quicknotes
quicknotes_group: quicknotes
quicknotes_home: /nonexistent
quicknotes_shell: /usr/sbin/nologin
quicknotes_binary_src: files/quicknotes
quicknotes_binary_dest: /usr/local/bin/quicknotes
quicknotes_service_name: quicknotes
quicknotes_service_unit_path: /etc/systemd/system/quicknotes.service
quicknotes_data_dir: /var/lib/quicknotes
quicknotes_data_path: /var/lib/quicknotes/notes.json
quicknotes_seed_path: /opt/quicknotes/app/seed.json
listen_addr: ":8080"
quicknotes_restart_sec: 4
ansible_pull_enabled: true
ansible_pull_repo_url: https://github.com/Hidancloud/DevOps-Intro.git
ansible_pull_repo_branch: feature/lab7
ansible_pull_checkout_dir: /var/lib/ansible-pull/quicknotes
ansible_pull_inventory_path: /etc/ansible/quicknotes-local.ini
ansible_pull_playbook_path: ansible/playbook.yaml
ansible_pull_service_name: ansible-pull-quicknotes.service
ansible_pull_timer_name: ansible-pull-quicknotes.timer

tasks:
- name: Create the quicknotes system group
ansible.builtin.group:
name: "{{ quicknotes_group }}"
system: true

- name: Create the quicknotes system user
ansible.builtin.user:
name: "{{ quicknotes_user }}"
group: "{{ quicknotes_group }}"
system: true
create_home: false
home: "{{ quicknotes_home }}"
shell: "{{ quicknotes_shell }}"

- name: Ensure the quicknotes data directory exists
ansible.builtin.file:
path: "{{ quicknotes_data_dir }}"
state: directory
owner: "{{ quicknotes_user }}"
group: "{{ quicknotes_group }}"
mode: "0750"

- name: Install the QuickNotes binary
ansible.builtin.copy:
src: "{{ quicknotes_binary_src }}"
dest: "{{ quicknotes_binary_dest }}"
owner: root
group: root
mode: "0755"
notify: restart quicknotes

- name: Install the systemd unit
ansible.builtin.template:
src: templates/quicknotes.service.j2
dest: "{{ quicknotes_service_unit_path }}"
owner: root
group: root
mode: "0644"
register: quicknotes_service_unit
notify: restart quicknotes

- name: Reload systemd when the unit changes
ansible.builtin.systemd:
daemon_reload: true
when: quicknotes_service_unit.changed

- name: Enable and start the quicknotes service
ansible.builtin.systemd:
name: "{{ quicknotes_service_name }}"
enabled: true
state: started

- name: Install ansible-pull dependencies
ansible.builtin.apt:
name:
- ansible
- git
state: present
update_cache: true
cache_valid_time: 3600
when: ansible_pull_enabled

- name: Ensure ansible config directory exists
ansible.builtin.file:
path: /etc/ansible
state: directory
owner: root
group: root
mode: "0755"
when: ansible_pull_enabled

- name: Install ansible-pull local inventory
ansible.builtin.template:
src: templates/quicknotes-local.ini.j2
dest: "{{ ansible_pull_inventory_path }}"
owner: root
group: root
mode: "0644"
register: ansible_pull_local_inventory
when: ansible_pull_enabled

- name: Ensure ansible-pull checkout directory exists
ansible.builtin.file:
path: "{{ ansible_pull_checkout_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: ansible_pull_enabled

- name: Install ansible-pull service
ansible.builtin.template:
src: templates/ansible-pull-quicknotes.service.j2
dest: "/etc/systemd/system/{{ ansible_pull_service_name }}"
owner: root
group: root
mode: "0644"
register: ansible_pull_service_unit
when: ansible_pull_enabled

- name: Install ansible-pull timer
ansible.builtin.template:
src: templates/ansible-pull-quicknotes.timer.j2
dest: "/etc/systemd/system/{{ ansible_pull_timer_name }}"
owner: root
group: root
mode: "0644"
register: ansible_pull_timer_unit
when: ansible_pull_enabled

- name: Reload systemd when ansible-pull units change
ansible.builtin.systemd:
daemon_reload: true
when:
- ansible_pull_enabled
- ansible_pull_service_unit.changed or ansible_pull_timer_unit.changed

- name: Prime ansible-pull once so the timer has a last-run anchor
ansible.builtin.systemd:
name: "{{ ansible_pull_service_name }}"
state: started
when:
- ansible_pull_enabled
- ansible_connection != 'local'
- ansible_pull_local_inventory.changed or ansible_pull_service_unit.changed or ansible_pull_timer_unit.changed

- name: Enable and start the ansible-pull timer
ansible.builtin.systemd:
name: "{{ ansible_pull_timer_name }}"
enabled: true
state: started
when: ansible_pull_enabled

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

[Service]
Type=oneshot
ExecStart=/usr/bin/ansible-pull -U {{ ansible_pull_repo_url }} -C {{ ansible_pull_repo_branch }} -d {{ ansible_pull_checkout_dir }} -i {{ ansible_pull_inventory_path }} {{ ansible_pull_playbook_path }}
10 changes: 10 additions & 0 deletions ansible/templates/ansible-pull-quicknotes.timer.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Run ansible-pull for QuickNotes every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit={{ ansible_pull_service_name }}

[Install]
WantedBy=timers.target
2 changes: 2 additions & 0 deletions ansible/templates/quicknotes-local.ini.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[quicknotes_vm]
quicknotes-vm ansible_connection=local ansible_python_interpreter=/usr/bin/python3
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 API
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User={{ quicknotes_user }}
Group={{ quicknotes_group }}
WorkingDirectory={{ quicknotes_data_dir }}
Environment=ADDR={{ listen_addr }}
Environment=DATA_PATH={{ quicknotes_data_path }}
Environment=SEED_PATH={{ quicknotes_seed_path }}
ExecStart={{ quicknotes_binary_dest }}
Restart=on-failure
RestartSec={{ quicknotes_restart_sec }}

[Install]
WantedBy=multi-user.target
Loading