diff --git a/.gitignore b/.gitignore index a91d60a..fa0dab3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ plan .terraform.lock.hcl generated.yml +# Secrets generated/persisted by ansible lookups (e.g. omnivore.yml) +ansible/plays/machine_setup/credentials/ + # terraform.tfvars should contain no secrets !terraform.tfvars diff --git a/ansible/plays/machine_setup/domain-watchdog.yml b/ansible/plays/machine_setup/domain-watchdog.yml new file mode 100644 index 0000000..3e163dc --- /dev/null +++ b/ansible/plays/machine_setup/domain-watchdog.yml @@ -0,0 +1,88 @@ +--- +- name: Set up domain-watchdog + hosts: domain-watchdog + become: yes + tasks: + - name: default_packages + ansible.builtin.include_tasks: ../../templates/default_packages.yml + + - name: create_user + ansible.builtin.include_tasks: ../../templates/create_user.yml + vars: + username: nick + github_user: Chewbaccalakis + + - name: Install Docker + ansible.builtin.include_tasks: ../../templates/install_docker.yml + + - name: Add nick to docker group + ansible.builtin.user: + name: nick + groups: + - docker + append: true + + - name: Deploy Domain Watchdog + vars: + domain_watchdog_dir: /opt/domain-watchdog + block: + - name: Clone domain-watchdog repo + ansible.builtin.git: + repo: https://github.com/maelgangloff/domain-watchdog.git + dest: "{{ domain_watchdog_dir }}" + version: master + update: yes + + - name: Create public/content directory + ansible.builtin.file: + path: "{{ domain_watchdog_dir }}/public/content" + state: directory + mode: "0755" + + # Pulled from Infisical, not generated/persisted to disk. Run this + # playbook as `infisical run -- ansible-playbook -i inventory ...` + # with DOMAIN_WATCHDOG_POSTGRES_PASSWORD, DOMAIN_WATCHDOG_APP_SECRET, + # and DOMAIN_WATCHDOG_JWT_PASSPHRASE secrets set in the workspace. + - name: Read secrets from environment (populated by `infisical run`) + ansible.builtin.set_fact: + domain_watchdog_postgres_password: "{{ lookup('ansible.builtin.env', 'DOMAIN_WATCHDOG_POSTGRES_PASSWORD') }}" + domain_watchdog_app_secret: "{{ lookup('ansible.builtin.env', 'DOMAIN_WATCHDOG_APP_SECRET') }}" + domain_watchdog_jwt_passphrase: "{{ lookup('ansible.builtin.env', 'DOMAIN_WATCHDOG_JWT_PASSPHRASE') }}" + no_log: true + + - name: Verify required secrets were provided + ansible.builtin.assert: + that: + - domain_watchdog_postgres_password | length > 0 + - domain_watchdog_app_secret | length > 0 + - domain_watchdog_jwt_passphrase | length > 0 + fail_msg: >- + DOMAIN_WATCHDOG_POSTGRES_PASSWORD, DOMAIN_WATCHDOG_APP_SECRET, and + DOMAIN_WATCHDOG_JWT_PASSPHRASE must be set. Run this playbook via + `infisical run -- ansible-playbook -i inventory ...` with those + secrets defined in the workspace. + + - name: Deploy .env file (docker compose variable substitution) + ansible.builtin.template: + src: templates/domain-watchdog.env.j2 + dest: "{{ domain_watchdog_dir }}/.env" + mode: "0600" + + - name: Deploy .env.local file (app secrets) + ansible.builtin.template: + src: templates/domain-watchdog.env.local.j2 + dest: "{{ domain_watchdog_dir }}/.env.local" + mode: "0600" + + - name: Pull prebuilt Domain Watchdog images + ansible.builtin.command: + cmd: docker compose pull + chdir: "{{ domain_watchdog_dir }}" + changed_when: false + + - name: Start Domain Watchdog via docker compose + ansible.builtin.command: + cmd: docker compose up -d + chdir: "{{ domain_watchdog_dir }}" + register: domain_watchdog_compose_up + changed_when: "'Started' in domain_watchdog_compose_up.stderr or 'Created' in domain_watchdog_compose_up.stderr" diff --git a/ansible/plays/machine_setup/grist.yml b/ansible/plays/machine_setup/grist.yml new file mode 100644 index 0000000..73da38c --- /dev/null +++ b/ansible/plays/machine_setup/grist.yml @@ -0,0 +1,86 @@ +--- +- name: Set up Grist + hosts: grist + become: yes + tasks: + - name: default_packages + ansible.builtin.include_tasks: ../../templates/default_packages.yml + + - name: create_user + ansible.builtin.include_tasks: ../../templates/create_user.yml + vars: + username: nick + github_user: Chewbaccalakis + + - name: Install Docker + ansible.builtin.include_tasks: ../../templates/install_docker.yml + + - name: Add nick to docker group + ansible.builtin.user: + name: nick + groups: + - docker + append: true + + - name: Deploy Grist + vars: + grist_dir: /opt/grist + grist_hostname: grist.atlashorizon.net + grist_oidc_issuer_host: auth.atlashorizon.net + block: + - name: Create persist directory + ansible.builtin.file: + path: "{{ grist_dir }}/persist" + state: directory + mode: "0755" + + - name: Deploy docker-compose.yml + ansible.builtin.copy: + src: templates/grist-docker-compose.yml + dest: "{{ grist_dir }}/docker-compose.yml" + mode: "0644" + + # Pulled from Infisical, not generated/persisted to disk. Run this + # playbook as `infisical run -- ansible-playbook -i inventory ...` + # with GRIST_SESSION_SECRET, GRIST_OIDC_IDP_CLIENT_ID, and + # GRIST_OIDC_IDP_CLIENT_SECRET secrets set in the workspace. + # + # The OIDC client must be registered in Authelia with redirect URI + # https://grist.atlashorizon.net/oauth2/callback. + - name: Read secrets from environment (populated by `infisical run`) + ansible.builtin.set_fact: + grist_session_secret: "{{ lookup('ansible.builtin.env', 'GRIST_SESSION_SECRET') }}" + grist_oidc_client_id: "{{ lookup('ansible.builtin.env', 'GRIST_OIDC_IDP_CLIENT_ID') }}" + grist_oidc_client_secret: "{{ lookup('ansible.builtin.env', 'GRIST_OIDC_IDP_CLIENT_SECRET') }}" + no_log: true + + - name: Verify required secrets were provided + ansible.builtin.assert: + that: + - grist_session_secret | length >= 32 + - grist_oidc_client_id | length > 0 + - grist_oidc_client_secret | length > 0 + fail_msg: >- + GRIST_SESSION_SECRET (>=32 chars), GRIST_OIDC_IDP_CLIENT_ID, and + GRIST_OIDC_IDP_CLIENT_SECRET must be set. Run this playbook via + `infisical run -- ansible-playbook -i inventory ...` with those + secrets defined in the workspace. + + - name: Deploy .env file (docker compose variable substitution) + ansible.builtin.template: + src: templates/grist.env.j2 + dest: "{{ grist_dir }}/.env" + mode: "0600" + + - name: Pull prebuilt Grist image + ansible.builtin.command: + cmd: docker compose pull + chdir: "{{ grist_dir }}" + changed_when: false + + - name: Start Grist via docker compose + ansible.builtin.command: + cmd: docker compose up -d + chdir: "{{ grist_dir }}" + register: grist_compose_up + changed_when: "'Started' in grist_compose_up.stderr or 'Created' in grist_compose_up.stderr" \ No newline at end of file diff --git a/ansible/plays/machine_setup/nametag.yml b/ansible/plays/machine_setup/nametag.yml new file mode 100644 index 0000000..264481e --- /dev/null +++ b/ansible/plays/machine_setup/nametag.yml @@ -0,0 +1,100 @@ +--- +- name: Set up nametag + hosts: nametag + become: yes + tasks: + - name: default_packages + ansible.builtin.include_tasks: ../../templates/default_packages.yml + + - name: create_user + ansible.builtin.include_tasks: ../../templates/create_user.yml + vars: + username: nick + github_user: Chewbaccalakis + + - name: Install Docker + ansible.builtin.include_tasks: ../../templates/install_docker.yml + + - name: Add nick to docker group + ansible.builtin.user: + name: nick + groups: + - docker + append: true + + - name: Deploy Nametag + vars: + nametag_dir: /opt/nametag + block: + - name: Clone nametag repo + ansible.builtin.git: + repo: https://github.com/mattogodoy/nametag.git + dest: "{{ nametag_dir }}" + version: master + update: yes + + - name: Create backups directory + ansible.builtin.file: + path: "{{ nametag_dir }}/backups" + state: directory + mode: "0755" + + # Pulled from Infisical, not generated/persisted to disk. Run this + # playbook as `infisical run -- ansible-playbook -i inventory ...` + # with NAMETAG_DB_PASSWORD, NAMETAG_NEXTAUTH_SECRET, + # NAMETAG_CRON_SECRET, and NAMETAG_REDIS_PASSWORD secrets set in + # the workspace. + - name: Read secrets from environment (populated by `infisical run`) + ansible.builtin.set_fact: + nametag_db_password: "{{ lookup('ansible.builtin.env', 'NAMETAG_DB_PASSWORD') }}" + nametag_nextauth_secret: "{{ lookup('ansible.builtin.env', 'NAMETAG_NEXTAUTH_SECRET') }}" + nametag_cron_secret: "{{ lookup('ansible.builtin.env', 'NAMETAG_CRON_SECRET') }}" + nametag_redis_password: "{{ lookup('ansible.builtin.env', 'NAMETAG_REDIS_PASSWORD') }}" + no_log: true + + - name: Verify required secrets were provided + ansible.builtin.assert: + that: + - nametag_db_password | length > 0 + - nametag_nextauth_secret | length >= 32 + - nametag_cron_secret | length >= 16 + - nametag_redis_password | length > 0 + fail_msg: >- + NAMETAG_DB_PASSWORD, NAMETAG_NEXTAUTH_SECRET (>=32 chars), + NAMETAG_CRON_SECRET (>=16 chars), and NAMETAG_REDIS_PASSWORD + must be set. Run this playbook via `infisical run -- + ansible-playbook -i inventory ...` with those secrets defined + in the workspace. + + - name: Deploy .env file + ansible.builtin.template: + src: templates/nametag.env.j2 + dest: "{{ nametag_dir }}/.env" + mode: "0600" + + # docker-compose.yml declares nametag-network and proxy as external + # networks, so docker compose up fails unless they already exist. + - name: Create external Docker networks required by docker-compose.yml + ansible.builtin.command: + cmd: "docker network create {{ item }}" + loop: + - nametag-network + - proxy + register: nametag_network_create + changed_when: "'Error' not in nametag_network_create.stderr" + failed_when: nametag_network_create.rc != 0 and 'already exists' not in nametag_network_create.stderr + loop_control: + label: "{{ item }}" + + - name: Pull prebuilt Nametag images + ansible.builtin.command: + cmd: docker compose pull + chdir: "{{ nametag_dir }}" + changed_when: false + + - name: Start Nametag via docker compose + ansible.builtin.command: + cmd: docker compose up -d + chdir: "{{ nametag_dir }}" + register: nametag_compose_up + changed_when: "'Started' in nametag_compose_up.stderr or 'Created' in nametag_compose_up.stderr" \ No newline at end of file diff --git a/ansible/plays/machine_setup/supernote.yml b/ansible/plays/machine_setup/supernote.yml new file mode 100644 index 0000000..678230c --- /dev/null +++ b/ansible/plays/machine_setup/supernote.yml @@ -0,0 +1,132 @@ +--- +- name: Set up Supernote Private Cloud + hosts: supernote + become: yes + tasks: + - name: default_packages + ansible.builtin.include_tasks: ../../templates/default_packages.yml + + - name: create_user + ansible.builtin.include_tasks: ../../templates/create_user.yml + vars: + username: nick + github_user: Chewbaccalakis + + - name: Install Docker + ansible.builtin.include_tasks: ../../templates/install_docker.yml + + - name: Add nick to docker group + ansible.builtin.user: + name: nick + groups: + - docker + append: true + + - name: Deploy Supernote Private Cloud + vars: + supernote_dir: /opt/supernote + block: + - name: Create data persistence directories + ansible.builtin.file: + path: "{{ supernote_dir }}/{{ item }}" + state: directory + mode: "0755" + loop: + - sndata/db_data + - sndata/config/mysql/conf.d + - sndata/redis_data + - sndata/recycle + - sndata/logs/cloud + - sndata/logs/app + - sndata/logs/web + - sndata/convert + - supernote_data + + # Downloaded fresh each run so upstream schema changes apply on + # reruns, per Supernote's own upgrade instructions. docker-compose.yml + # is vendored instead (see templates/supernote-docker-compose.yml) - + # the upstream file is missing the initdb mounts this playbook needs. + - name: Download Supernote database init script + ansible.builtin.get_url: + url: https://supernote-private-cloud.supernote.com/cloud/supernotedb.sql + dest: "{{ supernote_dir }}/supernotedb.sql" + mode: "0644" + force: yes + + - name: Deploy docker-compose.yml + ansible.builtin.copy: + src: templates/supernote-docker-compose.yml + dest: "{{ supernote_dir }}/docker-compose.yml" + mode: "0644" + + # Pulled from Infisical, not generated/persisted to disk. Run this + # playbook as `infisical run -- ansible-playbook -i inventory ...` + # with SUPERNOTE_MYSQL_ROOT_PASSWORD, SUPERNOTE_MYSQL_PASSWORD, + # SUPERNOTE_REDIS_PASSWORD, SUPERNOTE_SMTP_SERVER, SUPERNOTE_SMTP_USER, + # and SUPERNOTE_SMTP_PASSWORD secrets set in the workspace. + # SUPERNOTE_SMTP_PORT (default 465) and SUPERNOTE_SMTP_ENCRYPTION + # (default ssl) are optional. + - name: Read secrets from environment (populated by `infisical run`) + ansible.builtin.set_fact: + supernote_mysql_root_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_MYSQL_ROOT_PASSWORD') }}" + supernote_mysql_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_MYSQL_PASSWORD') }}" + supernote_redis_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_REDIS_PASSWORD') }}" + supernote_smtp_server: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_SERVER') }}" + supernote_smtp_port: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_PORT') }}" + supernote_smtp_user: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_USER') }}" + supernote_smtp_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_PASSWORD') }}" + supernote_smtp_encryption: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_ENCRYPTION') }}" + no_log: true + + - name: Verify required secrets were provided + ansible.builtin.assert: + that: + - supernote_mysql_root_password | length > 0 + - supernote_mysql_password | length > 0 + - supernote_redis_password | length > 0 + - supernote_smtp_server | length > 0 + - supernote_smtp_user | length > 0 + - supernote_smtp_password | length > 0 + fail_msg: >- + SUPERNOTE_MYSQL_ROOT_PASSWORD, SUPERNOTE_MYSQL_PASSWORD, + SUPERNOTE_REDIS_PASSWORD, SUPERNOTE_SMTP_SERVER, + SUPERNOTE_SMTP_USER, and SUPERNOTE_SMTP_PASSWORD must be set. + Run this playbook via `infisical run -- ansible-playbook -i + inventory ...` with those secrets defined in the workspace. + + - name: Deploy .env file (docker compose variable substitution) + ansible.builtin.template: + src: templates/supernote.env.j2 + dest: "{{ supernote_dir }}/.env" + mode: "0600" + + - name: Deploy SMTP config init script + ansible.builtin.template: + src: templates/supernote-email-init.sql.j2 + dest: "{{ supernote_dir }}/supernote-email-init.sql" + mode: "0600" + + - name: Pull prebuilt Supernote images + ansible.builtin.command: + cmd: docker compose pull + chdir: "{{ supernote_dir }}" + changed_when: false + + - name: Start Supernote Private Cloud via docker compose + ansible.builtin.command: + cmd: docker compose up -d + chdir: "{{ supernote_dir }}" + register: supernote_compose_up + changed_when: "'Started' in supernote_compose_up.stderr or 'Created' in supernote_compose_up.stderr" + + # docker-entrypoint-initdb.d scripts only run once, against an empty + # database, so reruns wouldn't otherwise pick up SMTP changes made in + # Infisical. Re-applying here keeps Infisical the source of truth. + - name: Apply SMTP configuration to the database + ansible.builtin.shell: + cmd: >- + docker compose exec -T -e MYSQL_PWD="{{ supernote_mysql_root_password }}" + mariadb mysql -u root supernotedb < {{ supernote_dir }}/supernote-email-init.sql + chdir: "{{ supernote_dir }}" + changed_when: false + no_log: true \ No newline at end of file diff --git a/ansible/plays/machine_setup/templates/domain-watchdog.env.j2 b/ansible/plays/machine_setup/templates/domain-watchdog.env.j2 new file mode 100644 index 0000000..c35b341 --- /dev/null +++ b/ansible/plays/machine_setup/templates/domain-watchdog.env.j2 @@ -0,0 +1,11 @@ +# Managed by Ansible (domain-watchdog.yml) - secrets sourced from Infisical +# via `infisical run -- ansible-playbook ...`. +# +# Consumed by docker compose for variable substitution in docker-compose.yml. + +POSTGRES_VERSION=16 +POSTGRES_DB=app +POSTGRES_USER=app +POSTGRES_PASSWORD={{ domain_watchdog_postgres_password }} + +SERVER_NAME=:80 diff --git a/ansible/plays/machine_setup/templates/domain-watchdog.env.local.j2 b/ansible/plays/machine_setup/templates/domain-watchdog.env.local.j2 new file mode 100644 index 0000000..3682593 --- /dev/null +++ b/ansible/plays/machine_setup/templates/domain-watchdog.env.local.j2 @@ -0,0 +1,16 @@ +# Managed by Ansible (domain-watchdog.yml) - secrets sourced from Infisical +# via `infisical run -- ansible-playbook ...`. +# +# Loaded into the app/worker containers via docker-compose.yml's env_file. + +APP_SECRET={{ domain_watchdog_app_secret }} +JWT_PASSPHRASE={{ domain_watchdog_jwt_passphrase }} + +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' + +MAILER_DSN=null://null +MAILER_SENDER_NAME="Domain Watchdog" +MAILER_SENDER_EMAIL=notifications@example.com + +REGISTRATION_ENABLED=true +REGISTRATION_VERIFY_EMAIL=false diff --git a/ansible/plays/machine_setup/templates/grist-docker-compose.yml b/ansible/plays/machine_setup/templates/grist-docker-compose.yml new file mode 100644 index 0000000..49fe6e2 --- /dev/null +++ b/ansible/plays/machine_setup/templates/grist-docker-compose.yml @@ -0,0 +1,12 @@ +# Managed by Ansible (grist.yml). Vendored here since grist-core has no +# docker-compose.yml at its repo root (see docker-compose-examples/ upstream). +services: + grist: + image: gristlabs/grist:latest + restart: unless-stopped + ports: + - "8484:8484" + volumes: + - ./persist:/persist + env_file: + - .env diff --git a/ansible/plays/machine_setup/templates/grist.env.j2 b/ansible/plays/machine_setup/templates/grist.env.j2 new file mode 100644 index 0000000..8b630a1 --- /dev/null +++ b/ansible/plays/machine_setup/templates/grist.env.j2 @@ -0,0 +1,19 @@ +# Managed by Ansible (grist.yml) - secrets sourced from Infisical via +# `infisical run -- ansible-playbook ...`. +# +# Consumed by docker compose for variable substitution in docker-compose.yml, +# and loaded directly into the Grist container via env_file. + +APP_HOME_URL=https://{{ grist_hostname }} +GRIST_IN_SERVICE=true +GRIST_SESSION_SECRET={{ grist_session_secret }} + +# OIDC via Authelia - client must be registered there with redirect URI +# https://{{ grist_hostname }}/oauth2/callback +GRIST_OIDC_IDP_ISSUER=https://{{ grist_oidc_issuer_host }} +GRIST_OIDC_IDP_CLIENT_ID={{ grist_oidc_client_id }} +GRIST_OIDC_IDP_CLIENT_SECRET={{ grist_oidc_client_secret }} + +# Authelia's OIDC discovery doc doesn't advertise end_session_endpoint, so +# Grist must be told not to call it on logout. +GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true diff --git a/ansible/plays/machine_setup/templates/nametag.env.j2 b/ansible/plays/machine_setup/templates/nametag.env.j2 new file mode 100644 index 0000000..d3a2f24 --- /dev/null +++ b/ansible/plays/machine_setup/templates/nametag.env.j2 @@ -0,0 +1,24 @@ +# Managed by Ansible (nametag.yml) - secrets sourced from Infisical via +# `infisical run -- ansible-playbook ...`. +# +# This file is loaded twice: docker compose reads it from the project +# directory for ${VAR} substitution in docker-compose.yml, and the app +# container also loads it directly via env_file. + +# Database +DB_HOST=db +DB_PORT=5432 +DB_NAME=nametag_db +DB_USER=nametag +DB_PASSWORD={{ nametag_db_password }} + +# Redis (rate limiting & caching) +REDIS_PASSWORD={{ nametag_redis_password }} +REDIS_URL=redis://:{{ nametag_redis_password }}@redis:6379 + +# Application URL +NEXTAUTH_URL=http://{{ ansible_host }}:3000 +NEXTAUTH_SECRET={{ nametag_nextauth_secret }} + +# Cron +CRON_SECRET={{ nametag_cron_secret }} diff --git a/ansible/plays/machine_setup/templates/omnivore.env.j2 b/ansible/plays/machine_setup/templates/omnivore.env.j2 new file mode 100644 index 0000000..0f109f3 --- /dev/null +++ b/ansible/plays/machine_setup/templates/omnivore.env.j2 @@ -0,0 +1,72 @@ +# Managed by Ansible (omnivore.yml) - generated secrets persist in +# ansible/plays/machine_setup/credentials/omnivore/ on the control node. + +# Postgres & Migrate + +POSTGRES_USER=postgres +POSTGRES_PASSWORD={{ omnivore_postgres_password }} +POSTGRES_DB=omnivore +PGPASSWORD={{ omnivore_postgres_password }} +PG_REPLICATION=false + +PG_HOST=postgres +PG_PASSWORD={{ omnivore_pg_password }} +PG_DB=omnivore +PG_USER=app_user +PG_PORT=5432 +PG_POOL_MAX=20 + +# API + +API_ENV=local +IMAGE_PROXY_SECRET={{ omnivore_image_proxy_secret }} +JWT_SECRET={{ omnivore_jwt_secret }} +SSO_JWT_SECRET={{ omnivore_sso_jwt_secret }} +GATEWAY_URL=http://api:8080/api +CONTENT_FETCH_URL=http://content-fetch:8080/?token={{ omnivore_verification_token }} +GCS_USE_LOCAL_HOST=true +GCS_UPLOAD_BUCKET=omnivore +AUTO_VERIFY=true +# Minio credentials are hardcoded in docker-compose.yml for the minio and +# createbuckets services, so these must match those literal values. +AWS_ACCESS_KEY_ID=minio +AWS_SECRET_ACCESS_KEY=miniominio +AWS_REGION=us-east-1 +CONTENT_FETCH_QUEUE_ENABLED=true + +IMAGE_PROXY_URL=http://{{ ansible_host }}:7070 +CLIENT_URL=http://{{ ansible_host }}:3000 +LOCAL_MINIO_URL=http://{{ ansible_host }}:1010 + +# Redis +REDIS_URL=redis://redis:6379/0 + +# Mail +WATCHER_API_KEY={{ omnivore_mail_watcher_key }} +LOCAL_EMAIL_DOMAIN=domain.tld +# SNS_ARN=arn_of_sns #for if you use SES and SNS for Email. + +# Web +APP_ENV=prod +BASE_URL=http://{{ ansible_host }}:3000 +SERVER_BASE_URL=http://{{ ansible_host }}:4000 +HIGHLIGHTS_BASE_URL=http://{{ ansible_host }}:3000 +# USE_DISCOVER_AI=true +# OPENAI_API_KEY= + +# Content Fetch +VERIFICATION_TOKEN={{ omnivore_verification_token }} +REST_BACKEND_ENDPOINT=http://api:8080/api +SKIP_UPLOAD_ORIGINAL=true + +# Minio +MINIO_ACCESS_KEY=minio +MINIO_SECRET_KEY=miniominio +AWS_S3_ENDPOINT_URL=http://minio:9000 + +# Export +EXPORT_QUEUE_NAME=omnivore-backend-queue + +# Youtube +YOUTUBE_TRANSCRIPT_PROMPT=Reformat the supplied transcript data adding formatting, puncuation, and paragraphs, but do not summarize or change the content. Format the output as markdown. +YOUTUBE_MAXIMUM_VIDEO_DURATION_TRANSCRIPT=1801 diff --git a/ansible/plays/machine_setup/templates/supernote-docker-compose.yml b/ansible/plays/machine_setup/templates/supernote-docker-compose.yml new file mode 100644 index 0000000..4650060 --- /dev/null +++ b/ansible/plays/machine_setup/templates/supernote-docker-compose.yml @@ -0,0 +1,101 @@ +# Vendored from https://supernote-private-cloud.supernote.com/cloud/docker-compose.yml, +# with two fixes applied: +# - the mariadb service is missing docker-entrypoint-initdb.d mounts in the +# upstream file, so supernotedb.sql would never actually load into a +# fresh database; added below. +# - a second init script (supernote-email-init.sql, rendered by Ansible +# from Infisical-sourced secrets) pre-seeds SMTP settings into +# u_email_config so registration/password-reset emails work without a +# manual trip through the web UI's "Email Settings" dialog. +# supernote-service's DB_HOSTNAME/MYSQL_*/REDIS_* env vars are also inlined +# directly here instead of upstream's separate .dbenv/env_file, since they're +# just ${VAR} substitutions from the same .env used by mariadb/redis. +services: + mariadb: + image: mariadb:10.6.24 + container_name: mariadb + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: supernotedb + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + TZ: ${TZ:-UTC} + command: --skip-name-resolve --bind-address=0.0.0.0 + volumes: + - ./sndata/db_data:/var/lib/mysql + - ./sndata/config/mysql/conf.d:/etc/mysql/conf.d:ro + - ./supernotedb.sql:/docker-entrypoint-initdb.d/00-supernotedb.sql:ro + - ./supernote-email-init.sql:/docker-entrypoint-initdb.d/01-supernote-email-init.sql:ro + - /etc/localtime:/etc/localtime:ro + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + restart: unless-stopped + + redis: + image: redis:7.4.7 + container_name: redis + environment: + TZ: ${TZ:-UTC} + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] + volumes: + - ./sndata/redis_data:/data + - /etc/localtime:/etc/localtime:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 3 + + notelib: + image: docker.io/supernote/notelib:6.9.3 + container_name: notelib + environment: + TZ: ${TZ:-UTC} + volumes: + - /etc/localtime:/etc/localtime:ro + restart: unless-stopped + + supernote-service: + image: docker.io/supernote/supernote-service:26.06.04 + container_name: supernote-service + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "19071:19071" + - "18072:18072" + - "${FRONTEND_PORT:-19072}:8080" + - "${DOMAIN_PORT:-8199}:8080" + environment: + EXTERNAL_PORT_BACKEND: "19071" + EXTERNAL_PORT_FRONTEND: ${FRONTEND_PORT:-19072} + EXTERNAL_PORT_DOMAIN: ${DOMAIN_PORT:-8199} + TZ: ${TZ:-UTC} + DB_HOSTNAME: mariadb + MYSQL_DATABASE: supernotedb + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: + - ./sndata/recycle:/home/supernote/recycle + - ./supernote_data:/home/supernote/data + - ./sndata/logs/cloud:/home/supernote/cloud/logs + - ./sndata/logs/app:/home/supernote/logs + - ./sndata/logs/web:/var/log/nginx + - ./sndata/convert:/home/supernote/convert + - /etc/localtime:/etc/localtime:ro + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" diff --git a/ansible/plays/machine_setup/templates/supernote-email-init.sql.j2 b/ansible/plays/machine_setup/templates/supernote-email-init.sql.j2 new file mode 100644 index 0000000..1969465 --- /dev/null +++ b/ansible/plays/machine_setup/templates/supernote-email-init.sql.j2 @@ -0,0 +1,30 @@ +-- Managed by Ansible (supernote.yml) - SMTP secrets sourced from Infisical +-- via `infisical run -- ansible-playbook ...`. +-- +-- Mounted into the mariadb container's docker-entrypoint-initdb.d/ so it +-- seeds u_email_config on first boot, and re-applied by the playbook via +-- `docker compose exec` on every run so Infisical stays the source of truth. +-- +-- u_email_config's schema/upsert key come from a third-party reverse +-- engineering of Supernote's supernotedb.sql (not officially documented) - +-- see https://github.com/liketheduck/supernote-private-cloud-docker. Verify +-- against Email Settings in the web UI if Supernote changes this schema. + +USE supernotedb; + +INSERT INTO u_email_config (smtp_server, port, user_name, password, encryption, flag) +VALUES ( + '{{ supernote_smtp_server }}', + '{{ supernote_smtp_port | default(465) }}', + '{{ supernote_smtp_user }}', + '{{ supernote_smtp_password }}', + '{{ supernote_smtp_encryption | default("ssl") }}', + 'Y' +) +ON DUPLICATE KEY UPDATE + smtp_server='{{ supernote_smtp_server }}', + port='{{ supernote_smtp_port | default(465) }}', + user_name='{{ supernote_smtp_user }}', + password='{{ supernote_smtp_password }}', + encryption='{{ supernote_smtp_encryption | default("ssl") }}', + flag='Y'; diff --git a/ansible/plays/machine_setup/templates/supernote.env.j2 b/ansible/plays/machine_setup/templates/supernote.env.j2 new file mode 100644 index 0000000..887ade3 --- /dev/null +++ b/ansible/plays/machine_setup/templates/supernote.env.j2 @@ -0,0 +1,10 @@ +# Managed by Ansible (supernote.yml) - secrets sourced from Infisical via +# `infisical run -- ansible-playbook ...`. +# +# Consumed by docker compose for ${VAR} substitution in docker-compose.yml. + +MYSQL_ROOT_PASSWORD={{ supernote_mysql_root_password }} +MYSQL_USER=supernote +MYSQL_PASSWORD={{ supernote_mysql_password }} + +REDIS_PASSWORD={{ supernote_redis_password }}