diff --git a/provision-contest/ansible/README.md b/provision-contest/ansible/README.md index 41c2fa47..b8493c8d 100644 --- a/provision-contest/ansible/README.md +++ b/provision-contest/ansible/README.md @@ -44,6 +44,7 @@ There are a few places where additional files should/can be added: * Machine/group specific local packages under `roles/base_packages/files/install-*/`. * Judgehost chroot local packages under `roles/judgedaemon/files/install-chroot/`. * The vendor dependencies under `roles/domjudge_checkout/files/vendor.tgz`. +* Machine/group specific docker containers under `roles/docker/files/containers-*/`. ## TODO diff --git a/provision-contest/ansible/admin.yml b/provision-contest/ansible/admin.yml index f36a6f78..40a84441 100644 --- a/provision-contest/ansible/admin.yml +++ b/provision-contest/ansible/admin.yml @@ -40,6 +40,8 @@ tags: clusterssh - role: phpstorm tags: phpstorm + - role: dj_notify + tags: dj_notify - role: prometheus_target_all tags: prometheus_target_all when: GRAFANA_MONITORING diff --git a/provision-contest/ansible/glitchtip.yml b/provision-contest/ansible/glitchtip.yml new file mode 100644 index 00000000..a9673f7c --- /dev/null +++ b/provision-contest/ansible/glitchtip.yml @@ -0,0 +1,21 @@ +--- +- hosts: glitchtip + vars: + host_type: glitchtip + become: true + handlers: + - include_tasks: handlers.yml + roles: + - role: base_packages + tags: base_packages + - role: icpc_fixes + tags: icpc_fixes + when: ICPC_IMAGE + - role: system_fixes + tags: system_fixes + - role: hosts + tags: hosts + - role: docker + tags: docker + - role: glitchtip + tags: glitchtip diff --git a/provision-contest/ansible/group_vars/onprem/secret.yml.example b/provision-contest/ansible/group_vars/onprem/secret.yml.example index 8e86e0ef..bdd179a5 100644 --- a/provision-contest/ansible/group_vars/onprem/secret.yml.example +++ b/provision-contest/ansible/group_vars/onprem/secret.yml.example @@ -69,3 +69,7 @@ PRESCLIENT_CONTEST: nwerc18 # Sentry DSN URL # SENTRY_DSN: + +# GlitchTip +# GLITCHTIP_SECRET: {some-strong-glitchtip-password} +# GLITCHTIP_PASSWORD: {some-strong-glitchtip-password} diff --git a/provision-contest/ansible/hosts.example b/provision-contest/ansible/hosts.example index b0f9cddb..837eb59e 100644 --- a/provision-contest/ansible/hosts.example +++ b/provision-contest/ansible/hosts.example @@ -61,7 +61,12 @@ domjudge-ccsadmin5 ansible_host=10.3.3.227 [grafana] # During the WFs we use one of the ccsadmin machines # Doesn't matter which (admin) machine but should not be 1 as that runs ansible -domjudge-ccsadmin2 ansible_host=10.3.3.225 +domjudge-ccsadmin2 ansible_host=10.3.3.224 + +[glitchtip] +# During the WFs we use one of the ccsadmin machines +# Doesn't matter which (admin) machine but should not be 1 as that runs ansible +domjudge-ccsadmin3 ansible_host=10.3.3.225 [cds] domjudge-cds ansible_host=10.2.2.228 diff --git a/provision-contest/ansible/roles/base_packages/files/install-docker/.gitignore b/provision-contest/ansible/roles/base_packages/files/install-docker/.gitignore new file mode 100644 index 00000000..c00df136 --- /dev/null +++ b/provision-contest/ansible/roles/base_packages/files/install-docker/.gitignore @@ -0,0 +1 @@ +*.deb diff --git a/provision-contest/ansible/roles/dj_notify/defaults/main.yml b/provision-contest/ansible/roles/dj_notify/defaults/main.yml new file mode 100644 index 00000000..1becf1d3 --- /dev/null +++ b/provision-contest/ansible/roles/dj_notify/defaults/main.yml @@ -0,0 +1,3 @@ +ALSA_DEVICE: alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink +NOTIFICATION_SOUND: /usr/share/sounds/sound-icons/trumpet-12.wav +NOTIFICATION_SOUND_VOLUME: 35536 diff --git a/provision-contest/ansible/roles/dj_notify/files/dj_notify.py b/provision-contest/ansible/roles/dj_notify/files/dj_notify.py new file mode 100644 index 00000000..74988427 --- /dev/null +++ b/provision-contest/ansible/roles/dj_notify/files/dj_notify.py @@ -0,0 +1,87 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +import json +import subprocess +import gi +import os +import webbrowser +import subprocess +import traceback +gi.require_version('Notify', '0.7') +from gi.repository import Notify + +HOSTNAME = "0.0.0.0" +PORT = 9999 +ALSA_DEVICE = os.environ['ALSA_DEVICE'] +NOTIFICATION_SOUND = os.environ['NOTIFICATION_SOUND'] +NOTIFICATION_SOUND_VOLUME = int(os.environ['NOTIFICATION_SOUND_VOLUME']) + + +def on_notification_closed(notification): + print(f"Notification {notification.id} closed.") + + +def on_link_click(notification, action, link): + webbrowser.open(link) + + +def filter_notification(title, body, link): + return not title.startswith("Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException") + + +class NotifyServer(BaseHTTPRequestHandler): + def create_notification(self, title, body, link): + notification = Notify.Notification.new(title, body) + notification.connect("closed", on_notification_closed) + notification.add_action( + "action_click", + "View in browser", + on_link_click, + link + ) + notification.show() + + + def notification_sound(self, sound): + # Use Popen to launch a non-blocking background process + subprocess.Popen(["paplay", "--volume", str(NOTIFICATION_SOUND_VOLUME), "--device", ALSA_DEVICE, sound]) + + + def do_POST(self): + length = int(self.headers.get('Content-Length')) + body = self.rfile.read(length) + content = json.loads(body) + print(json.dumps(content, indent=2)) + + att = content['attachments'][0] + title = att['title'] + link = att['title_link'] + body = att['text'] + + if filter_notification(title, body, link): + try: + self.create_notification(title, body, link) + except Exception: + print(traceback.format_exc()) + try: + self.notification_sound(NOTIFICATION_SOUND) + except Exception: + print(traceback.format_exc()) + + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.end_headers() + + self.wfile.write(bytes("ok", "utf-8")) + + +Notify.init("DOMjudge notifications") +server = HTTPServer((HOSTNAME, PORT), NotifyServer) + +try: + server.serve_forever() +except KeyboardInterrupt: + pass + +# Clean up +server.server_close() +Notify.uninit() diff --git a/provision-contest/ansible/roles/dj_notify/handlers/main.yml b/provision-contest/ansible/roles/dj_notify/handlers/main.yml new file mode 100644 index 00000000..b7ce19cb --- /dev/null +++ b/provision-contest/ansible/roles/dj_notify/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart dj_notify + systemd: + name: dj_notify + enabled: true + state: restarted + daemon_reload: true diff --git a/provision-contest/ansible/roles/dj_notify/tasks/main.yml b/provision-contest/ansible/roles/dj_notify/tasks/main.yml new file mode 100644 index 00000000..c6391e3d --- /dev/null +++ b/provision-contest/ansible/roles/dj_notify/tasks/main.yml @@ -0,0 +1,17 @@ +--- +# These tasks install the DOMjudge Notify script + +- name: Install dj_notify + copy: + src: "dj_notify.py" + dest: "/home/domjudge/bin/dj_notify.py" + owner: domjudge + group: domjudge + mode: 0755 + notify: Restart dj_notify + +- name: Copy dj_notify systemd unit file + template: + src: "dj_notify.service.j2" + dest: "/etc/systemd/system/dj_notify.service" + notify: Restart dj_notify diff --git a/provision-contest/ansible/roles/dj_notify/templates/dj_notify.service.j2 b/provision-contest/ansible/roles/dj_notify/templates/dj_notify.service.j2 new file mode 100644 index 00000000..fb4737e2 --- /dev/null +++ b/provision-contest/ansible/roles/dj_notify/templates/dj_notify.service.j2 @@ -0,0 +1,21 @@ +[Unit] +Description="DOMjudge Notify" +After=network.target + +[Service] +Type=simple + +Environment=ALSA_DEVICE={{ ALSA_DEVICE }} +Environment=NOTIFICATION_SOUND={{ NOTIFICATION_SOUND }} +Environment=DISPLAY=:0 +Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus +Environment=PULSE_SERVER=/run/user/1001/pulse/native +WorkingDirectory=/home/domjudge +ExecStart=/usr/bin/python3 -u /home/domjudge/bin/dj_notify.py +User=domjudge + +Restart=always +RestartSec=3 + +[Install] +WantedBy=graphical.target diff --git a/provision-contest/ansible/roles/docker/files/README.md b/provision-contest/ansible/roles/docker/files/README.md new file mode 100644 index 00000000..2eafe9e8 --- /dev/null +++ b/provision-contest/ansible/roles/docker/files/README.md @@ -0,0 +1,5 @@ +# Loading containers from archives +Any container `.tar` files placed in a `containers-` directory will be loaded as a container for the said host type. +The container will be tagged with the relative path starting from `containers-`, without the `.tar` file extension. +For example, the file `containers-glitchtip/glitchtip/glitchtip:v4.1.3.tar` will be loaded as a container tagged `glitchtip/glitchtip:v4.1.3` for the `glitchtip` host type. +Note that a nested directory structure is needed to tag containers which have both an organization and a container name. diff --git a/provision-contest/ansible/roles/docker/files/containers-glitchtip/.gitignore b/provision-contest/ansible/roles/docker/files/containers-glitchtip/.gitignore new file mode 100644 index 00000000..d874ad67 --- /dev/null +++ b/provision-contest/ansible/roles/docker/files/containers-glitchtip/.gitignore @@ -0,0 +1 @@ +*.tar diff --git a/provision-contest/ansible/roles/docker/files/containers-glitchtip/glitchtip/.gitignore b/provision-contest/ansible/roles/docker/files/containers-glitchtip/glitchtip/.gitignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/provision-contest/ansible/roles/docker/files/containers-glitchtip/glitchtip/.gitignore @@ -0,0 +1 @@ +* diff --git a/provision-contest/ansible/roles/docker/files/docker.asc b/provision-contest/ansible/roles/docker/files/docker.asc new file mode 100644 index 00000000..ee7872e5 --- /dev/null +++ b/provision-contest/ansible/roles/docker/files/docker.asc @@ -0,0 +1,62 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +YT90qFF93M3v01BbxP+EIY2/9tiIPbrd +=0YYh +-----END PGP PUBLIC KEY BLOCK----- diff --git a/provision-contest/ansible/roles/docker/tasks/load-container.yml b/provision-contest/ansible/roles/docker/tasks/load-container.yml new file mode 100644 index 00000000..1f4c6df2 --- /dev/null +++ b/provision-contest/ansible/roles/docker/tasks/load-container.yml @@ -0,0 +1,35 @@ +--- +- name: Load the container from archive if needed + block: + - name: Check for existing container + community.docker.docker_image_info: + name: "{{ img_name }}" + register: result + + - name: Transfer and load the container + block: + - name: Create temp container directory + file: + path: /tmp/dj_ansible + state: directory + owner: root + group: root + mode: 0700 + + - name: Transfer container archive + copy: + src: "{{ item.src }}" + dest: "{{ img_path }}" + owner: root + group: root + mode: 0700 + + - name: Import container from archive + community.docker.docker_image: + name: "{{ img_name }}" + load_path: "{{ img_path }}" + source: load + when: not result.images + vars: + img_name: "{{ item.path | splitext | first }}" + img_path: "/tmp/dj_ansible/{{ item.path | basename }}" diff --git a/provision-contest/ansible/roles/docker/tasks/main.yml b/provision-contest/ansible/roles/docker/tasks/main.yml new file mode 100644 index 00000000..537c7304 --- /dev/null +++ b/provision-contest/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Add Docker GPG apt Key + apt_key: + data: "{{ lookup('ansible.builtin.file', 'docker.asc') }}" + state: present + +- name: Add Docker Repository + when: not WF_RESTRICTED_NETWORK + apt_repository: + repo: deb https://download.docker.com/linux/ubuntu jammy stable + state: present + +- name: Install Docker and dependencies + apt: + state: present + install_recommends: false + pkg: + - containerd.io + - docker-buildx-plugin + - docker-ce + - docker-ce-cli + - docker-compose-plugin + - python3-pip + - python3-docker + +# The `runc` script interferes with running Docker containers. +# Mark it as not executable such that containers can run normally. +# If the machine does need the `runc` in the image, a custom fix has to be applied. +- name: Remove executable bit from "/usr/local/bin/runc" + when: ICPC_IMAGE + file: + dest: /usr/local/bin/runc + mode: -x + +- name: Load container archives + include_tasks: load-container.yml + with_filetree: + - files/containers-{{ host_type }}/ + when: item.state == 'file' and (item.path | splitext | last) == ".tar" diff --git a/provision-contest/ansible/roles/docker/vars/.gitignore b/provision-contest/ansible/roles/docker/vars/.gitignore new file mode 100644 index 00000000..1cda54be --- /dev/null +++ b/provision-contest/ansible/roles/docker/vars/.gitignore @@ -0,0 +1 @@ +*.yml diff --git a/provision-contest/ansible/roles/glitchtip/defaults/main.yml b/provision-contest/ansible/roles/glitchtip/defaults/main.yml new file mode 100644 index 00000000..df4a7c22 --- /dev/null +++ b/provision-contest/ansible/roles/glitchtip/defaults/main.yml @@ -0,0 +1,3 @@ +GLITCHTIP_PORT: 8000 +GLITCHTIP_TOKEN: +GLITCHTIP_WEBHOOK_DOMAIN: domjudge-ccsadmin2 diff --git a/provision-contest/ansible/roles/glitchtip/handlers/main.yml b/provision-contest/ansible/roles/glitchtip/handlers/main.yml new file mode 100644 index 00000000..61571dfd --- /dev/null +++ b/provision-contest/ansible/roles/glitchtip/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart GlitchTip + community.docker.docker_compose_v2: + project_src: /opt/glitchtip + files: + - docker-compose.yaml + state: restarted diff --git a/provision-contest/ansible/roles/glitchtip/tasks/create-monitor.yml b/provision-contest/ansible/roles/glitchtip/tasks/create-monitor.yml new file mode 100644 index 00000000..c8830a5a --- /dev/null +++ b/provision-contest/ansible/roles/glitchtip/tasks/create-monitor.yml @@ -0,0 +1,30 @@ +--- +- name: Fetch the project id + ansible.builtin.uri: + method: GET + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/teams/domjudge/DOMjudge/projects/" + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + register: glitchtip_proj +- name: Store JSON query in a fact due to escaping problems in the string below + set_fact: + TEMP_QUERY0: "[?name=='{{ item }}'].id" +- name: Create an DOMjudge uptime monitor for the project + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/organizations/domjudge/monitors/" + status_code: 201 + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + body: + expectedBody: "" + expectedStatus: 200 + timeout: 5 + interval: 2 + monitorType: "GET" + url: "https://{{ hostvars[item].ansible_host }}/public" + name: "{{ item }}" + project_id: "{{ glitchtip_proj.json | community.general.json_query(TEMP_QUERY0) | first }}" + body_format: json diff --git a/provision-contest/ansible/roles/glitchtip/tasks/main.yml b/provision-contest/ansible/roles/glitchtip/tasks/main.yml new file mode 100644 index 00000000..dbe16dc2 --- /dev/null +++ b/provision-contest/ansible/roles/glitchtip/tasks/main.yml @@ -0,0 +1,185 @@ +--- +- name: Create directories + file: + path: /opt/glitchtip + state: directory + +- name: Create compose file + template: + src: templates/docker-compose.yaml.j2 + dest: /opt/glitchtip/docker-compose.yaml + +- name: Deploy GlitchTip compose stack + community.docker.docker_compose_v2: + project_src: /opt/glitchtip + files: + - docker-compose.yaml + +- name: Assume we don't have an account if we didn't specify the token + when: not GLITCHTIP_TOKEN + block: + - name: Wait for stable GlitchTip migrations + ansible.builtin.wait_for: + timeout: 10 + + - name: Fetch CSRF from login page + ansible.builtin.uri: + method: GET + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/docs" + register: glitchtip_csrf + + - name: Register DOMjudge account + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/_allauth/browser/v1/auth/signup" + status_code: 200 + headers: + Cookie: "{{ glitchtip_csrf.cookies_string }}" + X-CSRFTOKEN: "{{ glitchtip_csrf.cookies_string | regex_search('csrftoken=([a-zA-Z0-9]+)', '\\1') | first }}" + body: + email: "team@domjudge.org" + password: "{{ GLITCHTIP_PASSWORD }}" + body_format: json + register: glitchtip_register + + - name: Create API token + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/api-tokens/" + status_code: 201 + headers: + Cookie: "{{ glitchtip_register.cookies_string }}" + X-CSRFTOKEN: "{{ glitchtip_register.cookies_string | regex_search('csrftoken=([a-zA-Z0-9]+)', '\\1') | first }}" + body: + label: "ansible" + scopes: [ + "project:read", + "project:write", + "project:admin", + "project:releases", + "team:read", + "team:write", + "team:admin", + "event:read", + "event:write", + "event:admin", + "org:read", + "org:write", + "org:admin", + "member:read", + "member:write", + "member:admin" + ] + body_format: json + register: glitchtip_token + + - name: Set API token + ansible.builtin.set_fact: + GLITCHTIP_TOKEN: "{{ glitchtip_token.json.token }}" + +- name: Check for existing organizations + ansible.builtin.uri: + method: GET + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/organizations/" + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + register: glitchtip_org + +- name: Create DOMjudge organization + when: glitchtip_org.json | community.general.json_query("[?name=='DOMjudge']") == [] + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/organizations/" + status_code: 201 + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + body: + name: "DOMjudge" + body_format: json + register: glitchtip_org + +- name: Check for existing teams + ansible.builtin.uri: + method: GET + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/organizations/domjudge/teams/" + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + register: glitchtip_team + +- name: Create DOMjudge team + when: glitchtip_team.json | community.general.json_query("[?slug=='DOMjudge']") == [] + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/organizations/domjudge/teams/" + status_code: 201 + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + body: + slug: "DOMjudge" + body_format: json + register: glitchtip_team + +- name: Check for existing projects + ansible.builtin.uri: + method: GET + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/teams/domjudge/DOMjudge/projects/" + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + register: glitchtip_proj + +- name: Create DOMjudge projects + when: glitchtip_proj.json | community.general.json_query("[?name=='{{ item }}']") == [] + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/teams/domjudge/DOMjudge/projects/" + status_code: 201 + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + body: + name: "{{ item }}" + platform: "php-symfony" + body_format: json + loop: "{{ ['setup-phase'] + groups['domserver'] }}" + +- name: Create project dj_notify webhook + when: glitchtip_proj.json | community.general.json_query("[?name=='{{ item }}']") == [] + ansible.builtin.uri: + method: POST + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/projects/domjudge/{{ item }}/alerts/" + status_code: 201 + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + body: + name: "dj_notify" + alertRecipients: + - recipientType: "webhook" + url: "http://{{ GLITCHTIP_WEBHOOK_DOMAIN }}:9999/" + timespanMinutes: 1 + quantity: 1 + uptime: true + body_format: json + loop: "{{ ['setup-phase'] + groups['domserver'] }}" + +- name: Check for existing monitors + ansible.builtin.uri: + method: GET + return_content: yes + url: "http://localhost:{{ GLITCHTIP_PORT }}/api/0/organizations/domjudge/monitors/" + headers: + Authorization: "Bearer {{ GLITCHTIP_TOKEN }}" + register: glitchtip_mon + +- name: Create DOMjudge monitors + when: glitchtip_mon.json | community.general.json_query("[?name=='{{ item }}']") == [] + loop: "{{ groups['domserver'] }}" + include_tasks: create-monitor.yml diff --git a/provision-contest/ansible/roles/glitchtip/templates/docker-compose.yaml.j2 b/provision-contest/ansible/roles/glitchtip/templates/docker-compose.yaml.j2 new file mode 100644 index 00000000..64e0898b --- /dev/null +++ b/provision-contest/ansible/roles/glitchtip/templates/docker-compose.yaml.j2 @@ -0,0 +1,57 @@ +# Uncomment version if using an older version of docker compose +# version: "3.8" +x-environment: + &default-environment + DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres + SECRET_KEY: {{ GLITCHTIP_SECRET }} + PORT: {{ GLITCHTIP_PORT }} + EMAIL_URL: consolemail:// + GLITCHTIP_DOMAIN: http://glitchtip:{{ GLITCHTIP_PORT }} + DEFAULT_FROM_EMAIL: email@glitchtip + CELERY_WORKER_AUTOSCALE: "1,1" + CELERY_WORKER_MAX_TASKS_PER_CHILD: "1000" + REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt + +x-depends_on: + &default-depends_on + - postgres + - redis + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_HOST_AUTH_METHOD: "trust" # Consider removing this and setting a password + restart: unless-stopped + volumes: + - pg-data:/var/lib/postgresql/data + redis: + image: redis:7-alpine + restart: unless-stopped + web: + image: glitchtip/glitchtip:v4.1.3 + depends_on: *default-depends_on + ports: + - {{ GLITCHTIP_PORT }}:{{ GLITCHTIP_PORT }} + environment: *default-environment + restart: unless-stopped + volumes: + - uploads:/code/uploads + worker: + image: glitchtip/glitchtip:v4.1.3 + command: ./bin/run-celery-with-beat.sh + depends_on: *default-depends_on + environment: *default-environment + restart: unless-stopped + volumes: + - uploads:/code/uploads + - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro + migrate: + image: glitchtip/glitchtip:v4.1.3 + depends_on: *default-depends_on + command: ./bin/run-migrate.sh + environment: *default-environment + +volumes: + pg-data: + uploads: diff --git a/provision-contest/ansible/roles/glitchtip/vars/.gitignore b/provision-contest/ansible/roles/glitchtip/vars/.gitignore new file mode 100644 index 00000000..1cda54be --- /dev/null +++ b/provision-contest/ansible/roles/glitchtip/vars/.gitignore @@ -0,0 +1 @@ +*.yml