diff --git a/PORTS b/PORTS --- a/PORTS +++ b/PORTS @@ -17,6 +17,8 @@ paas-docker 5000 Docker registry HTTP 9090 Openfire HTTP + 17080 Penpot - back-end + 17300 Penpot - exporter 19080 Nasqueron API - Datasources 20080 Nasqueron API - Docker registry API 22220 Phabricator Aphlict (client) diff --git a/_modules/convert.py b/_modules/convert.py --- a/_modules/convert.py +++ b/_modules/convert.py @@ -80,3 +80,10 @@ return salt.serializers.yaml.serialize( _to_dictionary(data, root), default_flow_style=False ) + + +def to_flags(data, enable_prefix="enable-", separator=" "): + """ + A function to convert a list of flags in a string to enable them. + """ + return separator.join([enable_prefix + item for item in data]) diff --git a/_tests/modules/test_convert.py b/_tests/modules/test_convert.py new file mode 100755 --- /dev/null +++ b/_tests/modules/test_convert.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +from importlib.machinery import SourceFileLoader +import unittest + +salt_test_case = SourceFileLoader("salt_test_case", "salt_test_case.py").load_module() +convert = SourceFileLoader("rust", "../_modules/convert.py").load_module() + + +class Testinstance(unittest.TestCase, salt_test_case.SaltTestCase): + def setUp(self): + self.initialize_mocks() + self.instance = convert + + def test_to_flags(self): + features = ["foo", "bar"] + + self.assertEqual("enable-foo enable-bar", convert.to_flags(features)) + + +if __name__ == "__main__": + unittest.main() diff --git a/pillar/credentials/vault.sls b/pillar/credentials/vault.sls --- a/pillar/credentials/vault.sls +++ b/pillar/credentials/vault.sls @@ -109,6 +109,10 @@ - ops/secrets/nasqueron/airflow/sentry - ops/secrets/dbserver/cluster-A/users/airflow + - ops/secrets/nasqueron/penpot/github + - ops/secrets/nasqueron/penpot/postgresql + - ops/secrets/nasqueron/penpot/secret_key + - ops/secrets/nasqueron/rabbitmq/white-rabbit/erlang-cookie - ops/secrets/nasqueron/rabbitmq/white-rabbit/root diff --git a/pillar/paas/docker/docker-002/penpot.sls b/pillar/paas/docker/docker-002/penpot.sls new file mode 100644 --- /dev/null +++ b/pillar/paas/docker/docker-002/penpot.sls @@ -0,0 +1,85 @@ +# ------------------------------------------------------------- +# Salt — Provision Docker engine +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Project: Nasqueron +# License: Trivial work, not eligible to copyright +# Service: Penpot +# Note: If compared with upstream installation method, +# the frontend part is skipped. This is to avoid +# PaaS nginx -> frontend nginx -> backend server. +# Frontend content is directly served by our nginx. +# ------------------------------------------------------------- + +docker_networks: + penpot: + subnet: 172.21.2.0/24 + +docker_images: + - penpotapp/backend + - penpotapp/exporter + +docker_containers: + + # + # Core services used by Penpot + # + + exim: + penpot_smtp: + mailname: mx.design.nasqueron.org + network: penpot + + postgresql: + penpot_db: + network: penpot + version: 15 + credential: nasqueron/penpot/postgresql + db: penpot + initdb_args: --data-checksums + + redis: + penpot_redis: + network: penpot + version: 7 + + # + # Penpot applications + # + + penpot_web: + penpot_web: + realm: penpot + network: penpot + host: design.nasqueron.org + app_port: 17080 + db: + uri: postgresql://penpot_db/penpot + services: + postgresql: penpot_db + redis: penpot_redis + smtp: penpot_smtp + exporter: http://localhost:17300 + credentials: + github: nasqueron/penpot/github + postgresql: nasqueron/penpot/postgresql + secret_key: nasqueron/penpot/secret_key + features: &features + # Features relevant for both frontend and backend + - registration + - login-with-password + - login-with-github + - secure-session-cookies + - webhooks + + # Features specific to the backend + - prepl-server + - smtp + + penpot_exporter: + penpot_exporter: + realm: penpot + network: penpot + app_port: 17300 + services: + frontend: https://design.nasqueron.org + redis: penpot_redis diff --git a/roles/paas-docker/containers/penpot_exporter.sls b/roles/paas-docker/containers/penpot_exporter.sls new file mode 100644 --- /dev/null +++ b/roles/paas-docker/containers/penpot_exporter.sls @@ -0,0 +1,32 @@ +# ------------------------------------------------------------- +# Salt — Provision Penpot +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Project: Nasqueron +# License: Trivial work, not eligible to copyright +# ------------------------------------------------------------- + +{% set containers = pillar["docker_containers"] %} + +{% for instance, container in containers["penpot_exporter"].items() %} + +# ------------------------------------------------------------- +# Container +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +{{ instance }}: + docker_container.running: + - detach: True + - interactive: True + - image: penpotapp/exporter + - networks: + - {{ container["network"] }} + - binds: /srv/{{ container["realm"] }}/assets:/opt/data/assets + - environment: + - PENPOT_PUBLIC_URI: {{ container["services"]["frontend"] }} + - PENPOT_REDIS_URI: redis://{{ container["services"]["redis"] }}/0 + - ports: + - 80 + - port_bindings: + - {{ container['app_port'] }}:80 + +{% endfor %} diff --git a/roles/paas-docker/containers/penpot_web.sls b/roles/paas-docker/containers/penpot_web.sls new file mode 100644 --- /dev/null +++ b/roles/paas-docker/containers/penpot_web.sls @@ -0,0 +1,119 @@ +# ------------------------------------------------------------- +# Salt — Provision Penpot +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Project: Nasqueron +# License: Trivial work, not eligible to copyright +# ------------------------------------------------------------- + +{% set has_selinux = salt["grains.get"]("selinux:enabled", False) %} +{% set containers = pillar["docker_containers"] %} + +{% for instance, container in containers["penpot_web"].items() %} + +{% set flags = salt["convert.to_flags"](container["features"]) %} + +# ------------------------------------------------------------- +# Storage directory +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/srv/{{ container["realm"] }}/assets: + file.directory: + - makedirs: True + - user: 1001 + - group: 1001 + +{% if has_selinux %} +selinux_context_penpot_data: + selinux.fcontext_policy_present: + - name: /srv/{{ container["realm"] }}/assets + - sel_type: container_file_t + +selinux_context_penpot_data_applied: + selinux.fcontext_policy_applied: + - name: /srv/{{ container["realm"] }}/assets +{% endif %} + +# ------------------------------------------------------------- +# Front-end assets +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/srv/{{ container["realm"] }}/public: + file.directory: + - makedirs: True + +penpot_{{ container["realm"] }}_public_content: + cmd.run: + - name: | + wget https://artifacts.nasqueron.org/penpot/penpot.tar.gz && \ + tar xzf penpot.tar.gz --strip 1 && \ + rm penpot.tar.gz + - cwd: /srv/{{ container["realm"] }}/public + - creates: /srv/{{ container["realm"] }}/public/version.txt + +/srv/{{ container["realm"] }}/public/js/config.js: + file.managed: + - mode: 444 + - contents: | + var penpotFlags = "{{ flags }}"; + +{% if has_selinux %} +selinux_context_penpot_public_data: + selinux.fcontext_policy_present: + - name: /srv/{{ container["realm"] }}/public + - sel_type: container_file_t + +selinux_context_penpot_public_data_applied: + selinux.fcontext_policy_applied: + - name: /srv/{{ container["realm"] }}/public +{% endif %} + +# ------------------------------------------------------------- +# Container +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +{{ instance }}: + docker_container.running: + - detach: True + - interactive: True + - image: penpotapp/backend + - networks: + - {{ container["network"] }} + - binds: /srv/{{ container["realm"] }}/assets:/opt/data/assets + - environment: + PENPOT_FLAGS: {{ flags }} + PENPOT_SECRET_KEY: {{ salt["credentials.get_password"](container["credentials"]["secret_key"]) }} + + PENPOT_PREPL_HOST: 0.0.0.0 + PENPOT_PUBLIC_URI: https://{{ container["host"] }} + + PENPOT_DATABASE_URI: postgresql://{{ container["services"]["postgresql"] }}/penpot + PENPOT_DATABASE_USERNAME: {{ salt["credentials.get_username"](container["credentials"]["postgresql"]) }} + PENPOT_DATABASE_PASSWORD: {{ salt["credentials.get_password"](container["credentials"]["postgresql"]) }} + + PENPOT_REDIS_URI: redis://{{ container["services"]["redis"] }}/0 + + PENPOT_ASSETS_STORAGE_BACKEND: assets-fs + PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets + + # Our privacy policy explicitly states we don't transfer data + # to third parties. + PENPOT_TELEMETRY_ENABLED: "false" + + {% if "smtp" in container["features"] %} + PENPOT_SMTP_HOST: {{ container["services"]["smtp"] }} + PENPOT_SMTP_PORT: 25 + PENPOT_SMTP_TLS: "false" + {% endif %} + PENPOT_SMTP_DEFAULT_FROM: no-reply@{{ container["host"] }} + PENPOT_SMTP_DEFAULT_REPLY_TO: no-reply@{{ container["host"] }} + + {% if "login-with-github" in container["features"] %} + PENPOT_GITHUB_CLIENT_ID: {{ salt["credentials.get_username"](container["credentials"]["github"]) }} + PENPOT_GITHUB_CLIENT_SECRET: {{ salt["credentials.get_password"](container["credentials"]["github"]) }} + {% endif %} + - ports: + - 6060 + - port_bindings: + - {{ container['app_port'] }}:6060 + +{% endfor %} diff --git a/roles/paas-docker/containers/postgresql.sls b/roles/paas-docker/containers/postgresql.sls --- a/roles/paas-docker/containers/postgresql.sls +++ b/roles/paas-docker/containers/postgresql.sls @@ -47,6 +47,15 @@ - environment: POSTGRES_USER: {{ salt['credentials.get_username'](container['credential']) }} POSTGRES_PASSWORD: {{ salt['credentials.get_password'](container['credential']) }} + + {% if 'db' in container %} + POSTGRES_DB: {{ container['db'] }} + {% endif %} + + {% if 'initdb_args' in container %} + POSTGRES_INITDB_ARGS: {{ container['initdb_args'] }} + {% endif %} + {% if 'network' in container %} - networks: - {{ container['network'] }} diff --git a/roles/paas-docker/nginx/files/vhosts/penpot_web.conf b/roles/paas-docker/nginx/files/vhosts/penpot_web.conf new file mode 100644 --- /dev/null +++ b/roles/paas-docker/nginx/files/vhosts/penpot_web.conf @@ -0,0 +1,106 @@ +# ------------------------------------------------------------- +# Configuration for Docker PaaS front-end nginx +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Source file: roles/paas-docker/nginx/files/vhosts/penpot_web.conf +# ------------------------------------------------------------- +# +# +# This file is managed by our rOPS SaltStack repository. +# +# Changes to this file may cause incorrect behavior +# and will be lost if the state is redeployed. +# + +# ------------------------------------------------------------- +# Application - {{ fqdn }} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +server { + listen 80; + listen [::]:80; + server_name {{ fqdn }}; + + include includes/letsencrypt; + + return 301 https://$host$request_uri; +} + +server { + server_name {{ fqdn }}; + + include includes/tls; + ssl_certificate /srv/letsencrypt/etc/live/{{ fqdn }}/fullchain.pem; + ssl_certificate_key /srv/letsencrypt/etc/live/{{ fqdn }}/privkey.pem; + + include includes/letsencrypt; + + client_max_body_size 100M; + charset utf-8; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + etag off; + + location @handle_redirect { + set $redirect_uri "$upstream_http_location"; + set $redirect_host "$upstream_http_x_host"; + set $redirect_cache_control "$upstream_http_cache_control"; + + proxy_buffering off; + + proxy_set_header Host "$redirect_host"; + proxy_hide_header etag; + proxy_pass $redirect_uri; + + add_header x-internal-redirect "$redirect_uri"; + add_header x-cache-control "$redirect_cache_control"; + add_header cache-control "$redirect_cache_control"; + } + + location /assets { + proxy_pass http://localhost:{{ app_port }}/assets; + recursive_error_pages on; + proxy_intercept_errors on; + error_page 301 302 307 = @handle_redirect; + } + + location /internal/assets { + internal; + alias /srv/{{ args["realm"] }}/assets; + add_header x-internal-redirect "$upstream_http_x_accel_redirect"; + } + + location /api/export { + proxy_pass {{ args["services"]["exporter"] }}; + } + + location /api { + proxy_pass http://localhost:{{ app_port }}/api; + } + + location /ws/notifications { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://localhost:{{ app_port }}/ws/notifications; + } + + location / { + location ~* \.(js|css).*$ { + add_header Cache-Control "max-age=86400" always; # 24 hours + } + + location ~* \.(html).*$ { + add_header Cache-Control "no-cache, max-age=0" always; + } + root /srv/{{ args["realm"] }}/public; + } + + root /var/wwwroot-502/_default; + error_page 502 /502.html; + location /502.html {} +}