Skip to content
This repository has been archived by the owner on May 5, 2023. It is now read-only.

feat!: support organization-specific domains for Portal backend and frontend [PORTAL-1067] #165

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
39 changes: 15 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,12 @@ The password to use to authenticate against the specified docker registry.

### Innoactive Portal

portal_hostname:
portal_domain:

**Mandatory** hostname under which the Portal will be available (this needs to be publicly reachable).

portal_alias_hostnames: []

Alternative or legacy hostnames array. Users accessing it will be redirected to portal_hostname.

admin_hostname:

**Mandatory** hostname under which the portal control panel will be available.

admin_alias_hostnames: []

Alternative or legacy hostnames array. Users accessing it will be redirected to admin_hostname.
**Mandatory** Domain under which the Portal's Backend, Control Panel and Frontend will be acessible (on a per organization basis à la `<organization>.<portal_domain>`).

customization_hostname:

**Mandatory** hostname under shich the customization service will be available.

customization_alias_hostnames: []

Alternative or legacy hostnames array. Users accessing it will be redirected to customization_hostname.

secret_key:
Expand Down Expand Up @@ -110,6 +94,10 @@ Optional Google Tag Manager Id. When set, Portal Backend / Control Panel will be

Optional mapping of additional environment variables to be passed on to the Portal Backend (e.g. to unlock hidden features).

extra_labels: {}

Optional mapping of additional labels to be passed on to the Portal Backend container.

extra_volumes: []

Optional mapping of additional volumes on the Portal Backend container.
Expand Down Expand Up @@ -392,10 +380,6 @@ The OAuth2 Client Secret that the Remote (Cloud Rendering) Launcher uses.

Whether or not to setup the Portal frontend for this instance. (Legacy parameter: `setup_discovery_portal: true`)

portal_hostname: "portal.{{ admin_configuration.primay_hostname }}"

The hostname under which the Portal frontend should be publicly availabe. This defaults to `portal.<hostname-of-portal-instance>`.

portal_oauth_client_id:

Allows to explicitly define the oauth client id to be used by the portal to communicate with the Portal backend. If not defined,
Expand All @@ -417,6 +401,10 @@ Optional Google Tag Manager Id. When set, Portal Frontend will be setup to load

Optional mapping of additional environment variables to be passed on to the Portal (e.g. to unlock hidden features).

portal_extra_labels: {}

Optional mapping of additional labels to be set on the Portal container.

#### Portal Customization Service

customization_image_version
Expand Down Expand Up @@ -445,6 +433,10 @@ an oauth client will automatically be retrieved.

Optional mapping of additional environment variables to be passed on to the Portal Backend (e.g. to unlock hidden features).

customization_extra_labels: {}

Optional mapping of additionallabels to be set on the Customization container.

### Mail Setup

In order to send mails, SMTP needs to be set up
Expand Down Expand Up @@ -536,10 +528,9 @@ users too:
setup_database: true
setup_control_panel: true
letsencrypt: true
portal_domain: innoactive.io
secret_key: not-secret-at-all-but-okay-for-tests
admin_email: [email protected]
portal_hostname: portal.my.hostname.com
admin_hostname: admin.portal.my.hostname.com
customization_hostname: customization.portal.my.hostname.com

## Testing
Expand Down
23 changes: 12 additions & 11 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ docker_registry:
password: "{{ registry_password }}"

# Main Portal Backend Configuration parameters
admin_hostname:
admin_alias_hostnames: []
portal_domain: "{{ undef(hint='You must specify the base domain for Portal') }}"
secret_key: "{{ undef(hint='You must specify a secret key for hashing / encryption') }}"
from_email:
admin_email: "{{ undef(hint='You must specify an e-mail address for the default admin user') }}"
Expand All @@ -26,6 +25,7 @@ sentry_dsn:
# Google Analytics configuration
google_analytics_tracking_id:
extra_environment_variables: {}
extra_labels: {}
extra_volumes: []

# Secure Communications (SSL / TLS)
Expand Down Expand Up @@ -85,8 +85,7 @@ image_versions:
cloudxr_management: "{{ cloudxr_management_image }}"

admin_configuration:
hostname: "{{ admin_hostname }}"
alias_hostnames: "{{ admin_alias_hostnames | trim }}"
domain: "{{ portal_domain }}"
secret_key: "{{ secret_key | mandatory }}"
from_email: "{{ from_email }}"
admin_email: "{{ admin_email | mandatory }}"
Expand All @@ -100,26 +99,28 @@ admin_configuration:
protocol: "{{ 'https' if traefik.enable_tls else 'http' }}"
create_admin_user: "{{ create_admin_user }}"
extra_environment_variables: "{{ extra_environment_variables }}"
extra_labels: "{{ extra_labels }}"
extra_volumes: "{{ extra_volumes }}"
google_tag_manager_id: "{{ google_tag_manager_id }}"
api_hostname: "api.{{ portal_domain }}"
admin_hostname: "admin.{{ portal_domain }}"

# Portal Configuration
portal_hostname:
portal_alias_hostnames: []
portal_oauth_client_id:
portal_sentry_dsn:
portal_signaling_service: ""
portal_extra_environment_variables: {}
portal_extra_labels: {}
portal_google_maps_api_key:
portal_google_tag_manager_id:

portal_configuration:
hostname: "{{ portal_hostname }}"
alias_hostnames: "{{ portal_alias_hostnames | trim }}"
domain: "{{ portal_domain }}"
oauth2_client_id: "{{ portal_oauth_client_id }}"
sentry_dsn: "{{ portal_sentry_dsn }}"
extra_environment_variables: "{{ portal_extra_environment_variables }}"
signaling_service: "{{ portal_signaling_service | default('wss://' + portal_hostname, true) }}"
extra_labels: "{{ portal_extra_labels }}"
signaling_service: "{{ portal_signaling_service | default('wss://signaling.' + portal_domain, true) }}"
google_maps_api_key: "{{ portal_google_maps_api_key }}"
google_tag_manager_id: "{{ portal_google_tag_manager_id }}"

Expand Down Expand Up @@ -213,17 +214,17 @@ desktop_client_configuration:

# Customization Configuration
customization_hostname:
customization_alias_hostnames: []
customization_oauth_client_id:
customization_oauth_client_secret:
customization_extra_environment_variables: {}
customization_extra_labels: {}

customization_configuration:
hostname: "{{ customization_hostname }}"
alias_hostnames: "{{ customization_alias_hostnames | trim }}"
oauth2_client_id: "{{ customization_oauth_client_id }}"
oauth2_client_secret: "{{ customization_oauth_client_secret }}"
extra_environment_variables: "{{ customization_extra_environment_variables }}"
extra_labels: "{{ customization_extra_labels }}"

# Mail Configuration
smtp_host:
Expand Down
3 changes: 1 addition & 2 deletions molecule/default/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
setup_control_panel: false
letsencrypt: false
secret_key: not-secret-at-all-but-okay-for-tests
admin_hostname: admin.portal.localhost
portal_hostname: portal.localhost
portal_domain: portal.localhost
customization_hostname: customization.portal.localhost
session_management_hostname: session-management.portal.localhost
admin_email: [email protected]
Expand Down
7 changes: 3 additions & 4 deletions molecule/saas/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
# do not issue certificates from production letsencrypt server
letsencrypt_test: true
secret_key: not-secret-at-all-but-okay-for-tests
admin_hostname: "admin.portal.{{ public_hostname }}"
portal_hostname: "portal.{{ public_hostname }}"
customization_hostname: "customization.portal.{{ public_hostname }}"
session_management_hostname: session-management.portal.{{ public_hostname }}"
portal_domain: "{{ public_hostname }}"
customization_hostname: "customization.{{ public_hostname }}"
session_management_hostname: session-management.{{ public_hostname }}"
admin_email: [email protected]
admin_password: sup3rs3cur3pa55w0rdf0rt3st1ng
session_management_ip_stack_api_token: invalid-token-but-not-empty
Expand Down
7 changes: 3 additions & 4 deletions molecule/with_cifs/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
# do not issue certificates from production letsencrypt server
letsencrypt_test: true
secret_key: not-secret-at-all-but-okay-for-tests
admin_hostname: "admin.portal.{{ public_hostname }}"
portal_hostname: "portal.{{ public_hostname }}"
customization_hostname: "customization.portal.{{ public_hostname }}"
session_management_hostname: session-management.portal.{{ public_hostname }}"
portal_domain: "{{ public_hostname }}"
customization_hostname: "customization.{{ public_hostname }}"
session_management_hostname: session-management.{{ public_hostname }}"
admin_email: [email protected]
admin_password: sup3rs3cur3pa55w0rdf0rt3st1ng
session_management_ip_stack_api_token: invalid-token-but-not-empty
Expand Down
41 changes: 6 additions & 35 deletions tasks/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@

- name: Collect environment variables for the main application server
set_fact:
traefik_admin_alternative_hostnames: "{{ admin_configuration.alias_hostnames | map('regex_replace', '^(.*)$', '`\\1`') | join(',') }}"
base_server_environment_variables:
DJANGO_ALLOWED_HOSTS: "{{ ([admin_configuration.hostname] + admin_configuration.alias_hostnames) | join(',') | mandatory }}"
DOMAIN: "{{ admin_configuration.domain }}"
DJANGO_SECRET_KEY: "{{ admin_configuration.secret_key | mandatory }}"
FROM_EMAIL: "{{ admin_configuration.from_email | default('admin@' + portal_configuration.hostname, true) }}"
FROM_EMAIL: "{{ admin_configuration.from_email | default('admin@' + admin_configuration.domain, true) }}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the sender is always [email protected] or something like that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the only way without new DKIM verification.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either that, or always [email protected] which is also weird ...
But yeah, definitely affects our terraform module ...
And we don't manage the innoactive.io dns records via terraform, so it's a bit of an issue.

RAVEN_DSN: "{{ admin_configuration.sentry_dsn | default('') }}"
GOOGLE_ANALYTICS_TRACKING_ID: "{{ admin_configuration.google_analytics_id | default('') }}"
EMAIL_HOST: "{{ mail_configuration.smtp.host }}"
Expand All @@ -24,46 +23,18 @@
EMAIL_USE_TLS: "{{ mail_configuration.smtp.use_tls | ternary('True', 'False') | string }}"
EMAIL_USE_SSL: "{{ mail_configuration.smtp.use_ssl | ternary('True', 'False') | string }}"
CUSTOMIZATION_SERVICE_API_URL: "{{ admin_configuration.protocol }}://{{ customization_configuration.hostname }}/api"
PORTAL_URL: "{{ admin_configuration.protocol }}://{{ portal_configuration.hostname }}"
CLOUD_RENDERING_CLIENT_APPLICATION_CLIENT_ID: "{{ desktop_client_configuration.remote_oauth2_client_id }}"
DEVICE_AUTHORIZATION_VERIFICATION_URL: "{{ admin_configuration.protocol }}://{{ portal_configuration.hostname }}/connect-hmd"
GOOGLE_TAG_MANAGER_ID: "{{ admin_configuration.google_tag_manager_id | default('') }}"

- name: Support alternative hostnames
set_fact:
alternative_admin_hostnames_traefik_labels:
# redirect alternative domains to primary
traefik.http.middlewares.redirect-alternative.redirectregex.regex: "(.*?://)([^/]+)(.*)"
traefik.http.middlewares.redirect-alternative.redirectregex.replacement: "${1}{{ admin_configuration.hostname }}${3}"
traefik.http.routers.web-alternative.rule: Host({{ traefik_admin_alternative_hostnames }})
traefik.http.routers.web-alternative.middlewares: redirect-alternative
traefik.http.routers.web-alternative.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.web-alternative.tls.certresolver: "{{ traefik.certificate_resolver }}"

# API, OAuth and Websocket Connections / Django-Channels
traefik.http.routers.web-api-alternative.rule: Host({{ traefik_admin_alternative_hostnames }}) && PathPrefix(`/ws`,`/api`,`/oauth`)
traefik.http.routers.web-api-alternative.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.web-api-alternative.tls.certresolver: "{{ traefik.certificate_resolver }}"
traefik.http.routers.web-api-alternative.priority: "100"

when: admin_configuration.alias_hostnames | default([]) | length > 0

- name: Start Django Application Server
vars:
traefik_labels:
traefik.enable: "true"
traefik.http.routers.web.rule: Host(`{{ admin_configuration.hostname }}`)
traefik.http.routers.web.rule: HostRegexp(`admin.{subdomain:[\w-_]+}.{{ admin_configuration.domain }}`,{{ admin_configuration.api_hostname }}`,`{{ admin_configuration.admin_hostname }}`)
traefik.http.routers.web.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.web.tls.certresolver: "{{ traefik.certificate_resolver }}"

# Websocket Connections / Django-Channels
traefik.http.routers.channels.rule: Host(`{{ admin_configuration.hostname }}`) && PathPrefix(`/ws`)
traefik.http.routers.channels.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.channels.tls.certresolver: "{{ traefik.certificate_resolver }}"
alternative_admin_domain_labels: "{{ alternative_admin_hostnames_traefik_labels | default({}) }}"
base_volumes:
- "{{ volume_names.media }}:/media"
redirect_http_labels: "{{ http_redirect_traefik_labels | default({}) }}"
docker_container:
name: "{{ container_names.backend }}"
image: "{{ image_versions.portal_backend }}"
Expand All @@ -72,7 +43,7 @@
restart_policy: unless-stopped
volumes: "{{ base_volumes + admin_configuration.extra_volumes }}"
env: "{{ base_server_environment_variables | combine(admin_configuration.extra_environment_variables) }}"
labels: "{{ traefik_labels | combine(alternative_admin_domain_labels) }}"
labels: "{{ traefik_labels | combine(admin_configuration.extra_labels) }}"
comparisons:
# correctly recreate container when any environment variable or labels is changed or added / removed
env: strict
Expand Down Expand Up @@ -110,8 +81,8 @@
shell: "{{ lookup('template', 'run_in_django_shell.sh.j2') }}" # noqa 305
vars:
python_script_name: change_site_name_and_domain
site_name: "{{ admin_configuration.hostname[:49] }}"
domain: "{{ admin_configuration.hostname }}"
site_name: "{{ admin_configuration.admin_hostname[:49] }}"
domain: "{{ admin_configuration.admin_hostname }}"
register: site_update_output
changed_when: site_update_output.stdout | trim | bool
tags:
Expand Down
2 changes: 1 addition & 1 deletion tasks/cloudxr_management_service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- 5000:5000
env:
Sentry__Dsn: "{{ cloudxr_configuration.sentry_dsn }}"
Sentry__ServerName: "{{ portal_configuration.hostname }}"
Sentry__ServerName: "{{ portal_domain }}"
Azure__Enabled: "{{ cloudxr_configuration.azure_enabled | ternary('true', 'false') }}"
Azure__ClientId: "{{ cloudxr_configuration.azure_client_id }}"
Azure__Secret: "{{ cloudxr_configuration.azure_client_secret }}"
Expand Down
35 changes: 6 additions & 29 deletions tasks/customization_service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
client_id: "{{ customization_configuration.oauth2_client_id }}"
client_secret: "{{ customization_configuration.oauth2_client_secret }}"
redirect_uris:
- "{{ admin_configuration.protocol }}://%s/services/customization/hub/callback/' % '{{ admin_configuration.hostname }}"
- "{{ admin_configuration.protocol }}://%s/microservices/customization/hub/callback/' % '{{ admin_configuration.hostname }}"
- "{{ admin_configuration.protocol }}://%s/hub_services/customization/hub/callback/' % '{{ admin_configuration.hostname }}"
- "{{ admin_configuration.protocol }}://%s/services/customization/hub/callback/' % '{{ admin_configuration.admin_hostname }}"
- "{{ admin_configuration.protocol }}://%s/microservices/customization/hub/callback/' % '{{ admin_configuration.admin_hostname }}"
- "{{ admin_configuration.protocol }}://%s/hub_services/customization/hub/callback/' % '{{ admin_configuration.admin_hostname }}"
- "{{ admin_configuration.protocol }}://%s/hub/callback/' % '{{ customization_configuration.hostname }}"
register: customization_oauth2_client_output
changed_when: customization_oauth2_client_output.stdout | from_json | json_query('changed')
Expand All @@ -37,32 +37,11 @@
docker_volume:
name: "{{ volume_names.customization }}"

- name: Support alternative hostnames
set_fact:
alternative_customization_hostnames_traefik_labels:
# redirect alternative domains to primary
traefik.http.middlewares.redirect-alternative-customization.redirectregex.regex: "(.*?://)([^/]+)(.*)"
traefik.http.middlewares.redirect-alternative-customization.redirectregex.replacement: "${1}{{ customization_configuration.hostname }}${3}"
traefik.http.routers.customization-alternative.rule: Host({{ alias_customization_hostnames }})
traefik.http.routers.customization-alternative.middlewares: redirect-alternative-customization
traefik.http.routers.customization-alternative.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.customization-alternative.tls.certresolver: "{{ traefik.certificate_resolver }}"

# API (cannot be redirected, because of failing CORS)
traefik.http.routers.customization-alternative-post.rule: Host({{ alias_customization_hostnames }}) && PathPrefix(`/api`)
traefik.http.routers.customization-alternative-post.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.customization-alternative-post.tls.certresolver: "{{ traefik.certificate_resolver }}"
vars:
# ensure alternative domains are supported in both old and new formats (old.domain and customization.portal.old.domain)
alternative_customization_hostnames: "{{ customization_configuration.alias_hostnames | map('regex_replace', '^(.*)$', '\\1') | list }}"
alias_customization_hostnames: "{{ alternative_customization_hostnames | map('regex_replace', '^(.*)$', '`\\1`') | join(',') }}"
when: customization_configuration.alias_hostnames | default([]) | length > 0

- name: Start Customization Service
vars:
CUSTOMIZATION_SERVICE_ALLOWED_HOSTS: "{{ ([customization_configuration.hostname] + customization_configuration.alias_hostnames) | join(',') | mandatory }}"
CUSTOMIZATION_SERVICE_ALLOWED_HOSTS: "{{ customization_configuration.hostname | mandatory }}"
default_environment_variables:
API_ROOT: "{{ admin_configuration.protocol }}://{{ admin_configuration.hostname }}"
API_ROOT: "{{ admin_configuration.protocol }}://{{ admin_configuration.api_hostname }}"
CUSTOMIZATION_API_ROOT: "{{ admin_configuration.protocol }}://{{ customization_configuration.hostname }}/api"
DB_HOST: db
DB_NAME: customization
Expand All @@ -80,8 +59,6 @@
traefik.http.routers.customization.rule: Host(`{{ customization_configuration.hostname }}`)
traefik.http.routers.customization.tls: "{{ traefik.enable_tls | ternary('true', 'false') }}"
traefik.http.routers.customization.tls.certresolver: "{{ traefik.certificate_resolver }}"
alternative_domain_labels: "{{ alternative_customization_hostnames_traefik_labels | default({}) }}"
redirect_http_labels: "{{ http_redirect_traefik_labels | default({}) }}"
docker_container:
name: "{{ container_names.customization }}"
image: "{{ image_versions.customization }}"
Expand All @@ -93,7 +70,7 @@
exposed_ports:
- "80"
env: "{{ default_environment_variables | combine(customization_configuration.extra_environment_variables) }}"
labels: "{{ traefik_labels | combine(alternative_domain_labels) }}"
labels: "{{ traefik_labels | combine(customization_configuration.extra_labels) }}"
comparisons:
# correctly recreate container when any environment variable or labels is changed or added / removed
env: strict
Expand Down
Loading