diff --git a/Makefile b/Makefile index 4aa7833..bfbdc19 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,8 @@ image: @: $${I} ifeq ($(I),composite) node tools/build-composite-image.js +else ifeq ($(I),app) + docker build . -f docker/$(I)/Dockerfile -t riju:$(I) else docker build . -f docker/$(I)/Dockerfile -t riju:$(I) --pull endif @@ -51,13 +53,24 @@ pkg: VOLUME_MOUNT ?= $(PWD) +P1 ?= 6119 +P2 ?= 6120 + +ifneq (,$(E)) +SHELL_PORTS := -p 127.0.0.1:$(P1):6119 -p 127.0.0.1:$(P2):6120 +else +SHELL_PORTS := +endif + .PHONY: shell shell: @: $${I} ifeq ($(I),admin) - docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock -v $(HOME)/.aws:/var/riju/.aws:ro -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e VOLUME_MOUNT=$(VOLUME_MOUNT) --network host riju:$(I) + docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock -v $(HOME)/.aws:/var/riju/.aws -v $(HOME)/.docker:/var/riju/.docker -v $(HOME)/.terraform.d:/var/riju/.terraform.d -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e VOLUME_MOUNT=$(VOLUME_MOUNT) $(SHELL_PORTS) --network host riju:$(I) +else ifeq ($(I),compile) + docker run -it --rm --hostname $(I) $(SHELL_PORTS) riju:$(I) else - docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -p 127.0.0.1:6119:6119 -p 127.0.0.1:6120:6120 riju:$(I) + docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src $(SHELL_PORTS) riju:$(I) endif .PHONY: install @@ -99,8 +112,6 @@ build: frontend system dev: make -j3 frontend-dev system-dev server-dev -### Run application code - ### Fetch artifacts from registries .PHONY: pull diff --git a/backend/server.js b/backend/server.js index c47ae87..8cbfd9c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -47,7 +47,7 @@ app.get("/:lang", (req, res) => { analyticsEnabled, }); } else { - res.send(`No such language: ${lang}`); + res.send(404, `No such language: ${lang}\n`); } }); app.use("/css", express.static("frontend/styles")); diff --git a/docker/admin/install.bash b/docker/admin/install.bash index 1724b89..4ff668b 100755 --- a/docker/admin/install.bash +++ b/docker/admin/install.bash @@ -11,6 +11,7 @@ apt-get update apt-get install -y curl gnupg lsb-release +curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - @@ -21,13 +22,33 @@ ubuntu_name="$(lsb_release -cs)" node_repo="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)" tee -a /etc/apt/sources.list.d/custom.list >/dev/null </dev/null </dev/null <&2 + exit 1 + fi + + IFS=" " read contents < "/tmp/id_${user}.pub" + echo "${contents}" > "/tmp/id_${user}.pub" +done + +sed -Ei 's/^#?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config +sed -Ei 's/^#?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config +sed -Ei 's/^#?PermitEmptyPasswords .*/PermitEmptyPasswords no/' /etc/ssh/sshd_config + +passwd -l root +useradd admin -g admin -G sudo -s /usr/bin/bash -p "$(echo "${ADMIN_PASSWORD}" | mkpasswd -s)" -m +useradd deploy -s /usr/bin/bash -p "!" + +for user in admin deploy; do + mkdir -p "/home/${user}/.ssh" + mv "/tmp/id_${user}.pub" "/home/${user}/.ssh/authorized_keys" + chown -R "${user}:${user}" "/home/${user}/.ssh" + chmod -R go-rwx "/home/${user}/.ssh" +done + +sed -i 's/^/command="sudo riju-deploy",restrict/' /home/deploy/.ssh/authorized_keys + +cat <<"EOF" > /etc/sudoers.d/riju +deploy ALL=(root) NOPASSWD: /usr/local/bin/riju-deploy +EOF diff --git a/packer/riju b/packer/riju new file mode 100755 index 0000000..f536156 --- /dev/null +++ b/packer/riju @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +domain="$(ls /etc/letsencrypt/live | grep -v README | head -n1)" + +if [[ -n "${domain}" ]]; then + echo "Detected cert for domain: ${domain}, enabling TLS" >&2 + export TLS=1 + TLS_PRIVATE_KEY="$(base64 "/etc/letsencrypt/live/${domain}/privkey.pem")" + TLS_CERTIFICATE="$(base64 "/etc/letsencrypt/live/${domain}/fullchain.pem")" + export TLS_PRIVATE_KEY TLS_CERTIFICATE + if [[ "${domain}" == riju.codes ]]; then + echo "Domain is riju.codes, enabling analytics" >&2 + export ANALYTICS=1 + else + echo "Domain is not riju.codes, disabling analytics" >&2 + fi +else + echo "No certs installed in /etc/letsencrypt/live, disabling TLS" >&2 +fi + +if [[ -n "${DETACH:-}" ]]; then + extra_args="-d" +elif [[ -t 1 ]]; then + extra_args="-it" +else + extra_args= +fi + +port_args="${PORT_MAPPING:--p 0.0.0.0:80:6119 -p 0.0.0.0:443:6120}" + +docker run --rm ${port_args} ${extra_args} \ + -e TLS -e TLS_PRIVATE_KEY -e TLS_CERTIFICATE -e ANALYTICS \ + -h riju --name "${CONTAINER_NAME:-riju-prod}" \ + "${IMAGE_NAME}:-riju:app" diff --git a/packer/riju-deploy b/packer/riju-deploy new file mode 100755 index 0000000..1a10308 --- /dev/null +++ b/packer/riju-deploy @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DOCKER_REPO="${DOCKER_REPO:-DOCKER_REPO_REPLACED_BY_PACKER}" + +if (( $# != 1 )); then + echo "usage: ssh deploy@riju COMMIT-SHA" >&2 + exit 1 +fi + +commit="$1" + +if [[ "$(echo -n "${commit}" | wc -c)" != 40 ]]; then + echo "riju-deploy: invalid commit SHA: ${commit}" >&2 + exit 1 +fi + +image="${DOCKER_REPO}:app-${commit}" + +echo "Pull image to be deployed..." +docker pull "${image}" + +echo "Start new image in test container..." >&2 +CONTAINER_NAME=riju-test IMAGE_NAME="${image}" DETACH=1 \ + PORT_MAPPING="-p 127.0.0.1:6119:6119" riju + +echo "Wait for web server to come up..." >&2 +sleep 10 + +echo "Test web server health..." >&2 +curl -fsSL http://localhost:6119 | head -n15 + +echo "Tear down test container..." >&2 +docker stop riju-test + +echo "Retag production image..." >&2 +docker tag "${image}" riju:app + +echo "Restart production server..." >&2 +systemctl restart riju diff --git a/packer/riju.service b/packer/riju.service new file mode 100644 index 0000000..6ea3ac0 --- /dev/null +++ b/packer/riju.service @@ -0,0 +1,12 @@ +[Unit] +Description=Riju online coding sandbox +Requires=docker.service +After=docker.service + +[Service] +Type=exec +ExecStart=riju +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/packer/server.json b/packer/server.json new file mode 100644 index 0000000..612e00a --- /dev/null +++ b/packer/server.json @@ -0,0 +1,70 @@ +{ + "variables": { + "docker_repo": "{{env `DOCKER_REPO`}}", + "admin_password": "{{env `ADMIN_PASSWORD`}}", + "admin_ssh_public_key_file": "{{env `ADMIN_SSH_PUBLIC_KEY_FILE`}", + "deploy_ssh_public_key_file": "{{env `DEPLOY_SSH_PUBLIC_KEY_FILE`}" + }, + "builders": [ + { + "type": "amazon-ebs", + "source_ami_filter": { + "filters": { + "virtualization-type": "hvm", + "root-device-type": "ebs", + "name": "ubuntu/images/ubuntu-groovy-20.10-amd64-server-*" + }, + "owners": ["099720109477"], + "most_recent": true + }, + "instance_type": "t3.micro", + "ssh_username": "ubuntu", + "ami_name": "riju-{{timestamp}}" + } + ], + "provisioners": [ + { + "type": "shell", + "script": "validate.bash", + "environment_vars": [ + "DOCKER_REPO={{user `docker_repo`}}", + "ADMIN_PASSWORD={{user `admin_password`}}", + "ADMIN_SSH_PUBLIC_KEY_FILE={{user `admin_ssh_public_key_file`}}", + "DEPLOY_SSH_PUBLIC_KEY_FILE={{user `deploy_ssh_public_key_file`}}" + ] + }, + { + "type": "file", + "source": "riju", + "destination": "/usr/local/bin/riju" + }, + { + "type": "file", + "source": "riju-deploy", + "destination": "/usr/local/bin/riju-deploy" + }, + { + "type": "file", + "source": "riju.service", + "destination": "/etc/systemd/system/riju.service" + }, + { + "type": "file", + "source": "{{user `admin_ssh_public_key_file`}}", + "destination": "/tmp/id_admin.pub" + }, + { + "type": "file", + "source": "{{user `deploy_ssh_public_key_file`}}", + "destination": "/tmp/id_deploy.pub" + }, + { + "type": "shell", + "script": "provision.bash", + "environment_vars": [ + "DOCKER_REPO={{user `docker_repo`}}", + "ADMIN_PASSWORD={{user `admin_password`}}" + ] + } + ] +} diff --git a/packer/validate.bash b/packer/validate.bash new file mode 100755 index 0000000..749cdc8 --- /dev/null +++ b/packer/validate.bash @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: ${DOCKER_REPO} +: ${ADMIN_PASSWORD} +: ${ADMIN_SSH_PUBLIC_KEY_FILE} +: ${DEPLOY_SSH_PUBLIC_KEY_FILE} diff --git a/system/compile.bash b/system/compile.bash index dbe7da1..e7c1cae 100755 --- a/system/compile.bash +++ b/system/compile.bash @@ -19,7 +19,9 @@ for src in system/src/*.c; do out="${out/.c}" verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}" if [[ "${out}" == *-privileged ]]; then - sudo chown root:riju "${out}" + if getent group riju >/dev/null; then + sudo chown root:riju "${out}" + fi sudo chmod a=,g=rx,u=rwxs "${out}" fi done diff --git a/tf/infra.tf b/tf/infra.tf index ab364d0..23464b5 100644 --- a/tf/infra.tf +++ b/tf/infra.tf @@ -13,15 +13,37 @@ terraform { } } +locals { + tags = { + Terraform = "Managed by Terraform" + } +} + +data "external" "env" { + program = ["jq", "-n", "env"] +} + provider "aws" { profile = "default" region = "us-west-1" } +data "aws_region" "current" {} + resource "aws_s3_bucket" "riju_debs" { bucket = "riju-debs" acl = "public-read" - tags = { - Terraform = "Managed by Terraform" - } + tags = local.tags +} + +resource "aws_instance" "server" { + instance_type = "t3.micro" + ami = data.external.env.result.AMI_ID + tags = local.tags +} + +resource "aws_ebs_volume" "data" { + availability_zone = "${data.aws_region.current.name}a" + size = 100 + tags = local.tags }