commit b9b7aa6a08c4fa701ef3b240f0b1f1d365eb610c Author: Jeff Clement Date: Fri Jul 18 23:12:35 2025 +0000 initial devcontainer setup diff --git a/.devcontainer/Caddyfile b/.devcontainer/Caddyfile new file mode 100644 index 0000000..4b2538c --- /dev/null +++ b/.devcontainer/Caddyfile @@ -0,0 +1,24 @@ +{ + # Disable the Caddy admin API for security + admin off +} + +# Main site block, listens on port 8001 +:5001 { + # Custom error handling for 502 Bad Gateway + handle_errors 502 { + root * /data # Serve files from /data + rewrite * /offline.html # Rewrite all requests to offline.html + templates # Enable template processing + file_server # Serve static files + } + # Route for database web UI + route /dev/db/* { + uri strip_prefix /dev/db # Remove /dev/db prefix + reverse_proxy 127.0.0.1:8081 # Proxy to DB UI on port 8081 + } + # Default route: proxy all other requests to main app + route /* { + reverse_proxy 127.0.0.1:4000 # Proxy to main app + } +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..4f2de9a --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/devcontainers/base:jammy + +# Get this thing up-to-date +RUN sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \ + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc|sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git curl inotify-tools wget imagemagick sqlite3 libsqlite3-dev libgdiplus postgresql-16 build-essential && \ + apt-get install -y build-essential autoconf m4 libncurses5-dev libwxgtk3.0-gtk3-dev libwxgtk-webview3.0-gtk3-dev libgl1-mesa-dev libglu1-mesa-dev libpng-dev libssh-dev unixodbc-dev xsltproc fop libxml2-utils libncurses-dev openjdk-11-jdk watchman + +# Install MISE +RUN curl https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh && \ + echo 'eval "$(/usr/local/bin/mise activate bash)"' >> /home/vscode/.bashrc && \ + echo 'eval "$(/usr/local/bin/mise activate bash --shims)"' >> /home/vscode/.bash_profile + +# locale +ENV LANG en_US.UTF-8 +ENV LC_ALL en_US.UTF-8 +ENV LANGUAGE en_US:en + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2c5c96c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,61 @@ +{ + "name": "Ubuntu", + + "dockerComposeFile": "docker-compose.yml", + "service": "app", + + "workspaceFolder": "/workspace", + + "features": { + "ghcr.io/devcontainers/features/git:1": {} + }, + + "postCreateCommand": "bash .devcontainer/post_create.sh", + + "forwardPorts": [ + 5001 + ], + + "portsAttributes": { + "5001": { + "label": "PROXY" + } + }, + + "remoteEnv": { + "PROJECT_ROOT": "${containerWorkspaceFolder}", + "MISE_ENV": "dev,devcontainer" + }, + + "customizations": { + "vscode": { + "settings": { + "sqltools.connections": [ + { + "name": "Development Database", + "driver": "PostgreSQL", + "previewLimit": 50, + "server": "postgres", + "port": 5432, + "database": "app", + "username": "postgres", + "password": "postgres" + } + ] + }, + "extensions": [ + "github.codespaces", + "tamasfe.even-better-toml", + "phoenixframework.phoenix", + "JakeBecker.elixir-ls", + "mechatroner.rainbow-csv", + "mikestead.dotenv", + "bradlc.vscode-tailwindcss", + "github.vscode-github-actions", + "humao.rest-client", + "mtxr.sqltools-driver-pg", + "mtxr.sqltools" + ] + } + } +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..c4944d5 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3' + +# NOTE on `network_mode` +# All of the containers are bound together and share the same network context as the DB container. +# This means that each service needs to be listening to a unique port since they are all effectively +# mount to the same interface. i.e. Execute can connect to postgres on 127.0.0.1:5432 even though +# they are separate containers. Note that if, for some reason, Postgres fails to start, the other +# containers are going to have all manner of mysterious network connectivity issues. + +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ..:/workspace:cached + depends_on: + - postgres + + environment: + + # Configure MISE to use DEV and DEVCONTAINER environments + MISE_ENV: dev,devcontainer + + # Setup postgres environment variables + PGUSER: postgres + PGDATABASE: app + PGPASSWORD: postgres + PGHOST: postgres + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + network_mode: service:postgres + + caddy: + image: caddy:latest + restart: unless-stopped + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ./offline.html:/data/offline.html + network_mode: service:postgres + + pgweb: + image: sosedoff/pgweb + restart: unless-stopped + environment: + - PGWEB_DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app?sslmode=disable + - PGWEB_LOCK_SESSION=true # disable connect/disconnect buttons since they aren't useful here + depends_on: + - postgres # my database container is called postgres, not db + network_mode: service:postgres + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_DB: app + POSTGRES_PASSWORD: postgres + healthcheck: + test: pg_isready -U postgres -h 127.0.0.1 + interval: 5s \ No newline at end of file diff --git a/.devcontainer/nuke_db.sh b/.devcontainer/nuke_db.sh new file mode 100755 index 0000000..ba9d5aa --- /dev/null +++ b/.devcontainer/nuke_db.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + + +echo " - Clearing existing objects" +# Clean existing objects using schema drop +psql -d ${PGDATABASE} -v ON_ERROR_STOP=1 <<-EOSQL > /dev/null 2>&1 + DROP SCHEMA PUBLIC CASCADE; + CREATE SCHEMA PUBLIC; +EOSQL + +echo "Database nuked successfully" diff --git a/.devcontainer/offline.html b/.devcontainer/offline.html new file mode 100644 index 0000000..d1dc216 --- /dev/null +++ b/.devcontainer/offline.html @@ -0,0 +1,38 @@ + + + + + + Service Unavailable + + + + +
+

Service Unavailable

+

The service is currently unavailable.

+

This page will refresh every 2 seconds.

+
+ + diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh new file mode 100644 index 0000000..5094945 --- /dev/null +++ b/.devcontainer/post_create.sh @@ -0,0 +1,16 @@ +# Change to the project root directory +cd $PROJECT_ROOT + +# Trust all tools and plugins defined in mise.toml +mise trust --all + +# Install all tools and plugins defined in mise.toml +mise install + +# Activate mise environment for bash +eval "$(mise activate bash)" + +# Run 'mix setup' only if mix.exs exists (i.e., if this is an Elixir project) +if [ -f mix.exs ]; then + mix setup +fi \ No newline at end of file diff --git a/.devcontainer/restore_db.sh b/.devcontainer/restore_db.sh new file mode 100755 index 0000000..2ad7a6d --- /dev/null +++ b/.devcontainer/restore_db.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +set -e + +safe_mode=false + +# Parse command line arguments +while getopts ":s" opt; do + case $opt in + s) + safe_mode=true + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +# Check if safe mode is enabled +if [ "$safe_mode" = true ]; then + # Count objects in public schema + object_count=$(psql -d ${PGDATABASE} -t -c "SELECT COUNT(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public';" | tr -d ' ') + + if [ "$object_count" -gt 0 ]; then + echo "Safe mode: Public schema is not empty. Aborting import." + exit 1 + fi +fi + + +# Shift the options so that $1 is the input path +shift $((OPTIND-1)) + +# If we were't given any arguments, make the user pick one! +if [ $# -eq 0 ]; then + INPUT_PATH=$(find $PROJECT_ROOT/.snapshots -type f -name "*.sql" | sort -r | fzf --no-sort --header="Select database to restore") +else + INPUT_PATH="$1" +fi + +# Determine if input is directory or file +if [ -d "$INPUT_PATH" ]; then + # Find latest SQL file by name sorting (assuming timestamped filenames) + LATEST_FILE=$(ls "$INPUT_PATH"/*.sql 2>/dev/null | sort -r | head -n 1) + + if [ -z "$LATEST_FILE" ]; then + echo "No .sql files found in directory" + exit 1 + fi + + DUMP_FILE="$LATEST_FILE" +elif [ -f "$INPUT_PATH" ]; then + DUMP_FILE="$INPUT_PATH" +else + echo "Invalid path: $INPUT_PATH" + exit 1 +fi + +# Validate the file exists +if [ ! -f "$DUMP_FILE" ]; then + echo "SQL file not found: $DUMP_FILE" + exit 1 +fi + +echo "Using dump file: $DUMP_FILE" + +echo " - Clearing existing objects" +# Clean existing objects using schema drop +psql -d ${PGDATABASE} -v ON_ERROR_STOP=1 <<-EOSQL > /dev/null 2>&1 + DROP SCHEMA PUBLIC CASCADE; + CREATE SCHEMA PUBLIC; +EOSQL + +echo " - Importing database" +# Import new dump +psql -d ${PGDATABASE} -v ON_ERROR_STOP=1 -f "$DUMP_FILE" > /dev/null 2>&1 + +echo "Database imported successfully from: $DUMP_FILE" diff --git a/.devcontainer/snap_db.sh b/.devcontainer/snap_db.sh new file mode 100755 index 0000000..38e251c --- /dev/null +++ b/.devcontainer/snap_db.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Ensure the .snapshots directory exists +mkdir -p "$PROJECT_ROOT/.snapshots" + +# Generate the base filename with timestamp +base_filename="$PROJECT_ROOT/.snapshots/snapshot_$(date +%Y%m%d_%H%M%S)" + +# Initialize an empty string for sanitized arguments +sanitized_args="" + +# Loop through all arguments +for arg in "$@"; do + # Sanitize the argument: remove spaces and special characters + sanitized=$(echo "$arg" | tr -cd '[:alnum:]_-') + sanitized_args="${sanitized_args}_${sanitized}" +done + +# Remove leading underscore if present +sanitized_args=${sanitized_args#_} + +# Create the final filename +if [ -n "$sanitized_args" ]; then + filename="${base_filename}_${sanitized_args}.sql" +else + filename="${base_filename}.sql" +fi + +# Run pg_dump and save to the generated filename +pg_dump --no-tablespaces ${PGDATABASE} -f "$filename" + +# Print the filename that was written +echo "Snapshot saved to: $filename" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f193e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Database Snapshots go here. +.snapshots + +# Never capture environment secrets! +.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e5b7f7 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Phoenix / DevContainer starter project + +## Initializing a new Phoenix Project + +## Initializing a new Ash Project + +``` +mix archive.install hex igniter_new --force +mix archive.install hex phx_new 1.8.0-rc.4 --force + +mix igniter.new pimento_cheese --with phx.new --install ash,ash_phoenix \ + --install ash_json_api,ash_postgres \ + --install ash_authentication,ash_authentication_phoenix \ + --install ash_admin,ash_oban --install oban_web,ash_archival \ + --install live_debugger,ash_paper_trail --install ash_ai,cloak \ + --install ash_cloak --auth-strategy magic_link --yes +``` + +* Merge `.gitignore` from new project directory into `/workspace` +* Move the rest of files from project directory to `/workspace` +* Update `config/dev.exs` database to `app` +* Update `config/dev.app` database to `test#{System.get_env("MIX_TEST_PARTITION")}"` \ No newline at end of file diff --git a/mise.dev.toml b/mise.dev.toml new file mode 100644 index 0000000..c99a5ba --- /dev/null +++ b/mise.dev.toml @@ -0,0 +1,9 @@ +[tools] +fzf = "latest" +ripgrep = "latest" +jq = "latest" + +python = "latest" +pipx = "latest" +"pipx:httpie" = "latest" +"pipx:pgcli" = "latest" \ No newline at end of file diff --git a/mise.devcontainer.toml b/mise.devcontainer.toml new file mode 100644 index 0000000..dca2db9 --- /dev/null +++ b/mise.devcontainer.toml @@ -0,0 +1,23 @@ +[tools] + +[env] + + +[tasks.snapshot] +description="Take a database snapshot" +alias="snap" +run = ".devcontainer/snap_db.sh" + +[tasks.restore] +description="Restore database to any database / snapshot" +run = ".devcontainer/restore_db.sh" + +[tasks.nuke] +description="Nuke the database" +run = ".devcontainer/nuke_db.sh" + +[tasks.db] +description="Launch Postgres CLI" +run = "pgcli --less-chatty" + + diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..cd8e3f9 --- /dev/null +++ b/mise.toml @@ -0,0 +1,4 @@ +[tools] +erlang = "28" +elixir = "1.18" +node = "latest"