mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-04 12:18:17 +00:00
Compare commits
1 Commits
feature/sh
...
aa1a901309
| Author | SHA1 | Date | |
|---|---|---|---|
| aa1a901309 |
@@ -114,7 +114,7 @@ defaults_networks:
|
||||
subnet: 192.168.104.48/28
|
||||
web-app-mini-qr:
|
||||
subnet: 192.168.104.64/28
|
||||
web-app-shopware:
|
||||
web-app-drupal:
|
||||
subnet: 192.168.104.80/28
|
||||
|
||||
# /24 Networks / 254 Usable Clients
|
||||
|
||||
@@ -81,7 +81,7 @@ ports:
|
||||
web-app-minio_api: 8057
|
||||
web-app-minio_console: 8058
|
||||
web-app-mini-qr: 8059
|
||||
web-app-shopware: 8060
|
||||
web-app-drupal: 8060
|
||||
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
|
||||
public:
|
||||
# The following ports should be changed to 22 on the subdomain via stream mapping
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{# ------------------------------------------------------------------------------
|
||||
Healthcheck: HTTP Local
|
||||
------------------------------------------------------------------------------
|
||||
This template defines a generic HTTP healthcheck for containers exposing
|
||||
a web service on a local port (e.g., Nginx, Apache, PHP-FPM, Shopware, etc.).
|
||||
|
||||
It uses `wget` or `curl` (as fallback) to test if the container responds on
|
||||
http://127.0.0.1:{{ container_port }}/. If the request succeeds, Docker marks
|
||||
the container as "healthy"; otherwise, as "unhealthy".
|
||||
|
||||
Parameters:
|
||||
- container_port: The internal port the service listens on.
|
||||
|
||||
Timing:
|
||||
- interval: 30s → Check every 30 seconds
|
||||
- timeout: 5s → Each check must complete within 5 seconds
|
||||
- retries: 5 → Mark unhealthy after 5 consecutive failures
|
||||
- start_period: 20s → Grace period before health checks begin
|
||||
|
||||
Usage:
|
||||
{% filter indent(4) %}
|
||||
{% include 'roles/docker-container/templates/healthcheck/http.yml.j2' %}
|
||||
{% endfilter %}
|
||||
------------------------------------------------------------------------------
|
||||
#}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:{{ container_port }}/ >/dev/null || curl -fsS http://127.0.0.1:{{ container_port }}/ >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
@@ -6,7 +6,7 @@ entity_name: "{{ application_id | get_entity_name }
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
# Docker Compose
|
||||
database_type: "{{ entity_name }}"
|
||||
database_type: "{{ application_id | get_entity_name }}"
|
||||
|
||||
## Postgres
|
||||
POSTGRES_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
|
||||
@@ -4,13 +4,11 @@ __metaclass__ = type
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
"""
|
||||
Group the given cards into categorized and uncategorized lists
|
||||
based on the tags from menu_categories.
|
||||
Categories are sorted alphabetically before returning.
|
||||
"""
|
||||
if len(terms) < 2:
|
||||
raise AnsibleError("Missing required arguments")
|
||||
@@ -21,7 +19,6 @@ class LookupModule(LookupBase):
|
||||
categorized = {}
|
||||
uncategorized = []
|
||||
|
||||
# Categorize cards
|
||||
for card in cards:
|
||||
found = False
|
||||
for category, data in menu_categories.items():
|
||||
@@ -32,14 +29,10 @@ class LookupModule(LookupBase):
|
||||
if not found:
|
||||
uncategorized.append(card)
|
||||
|
||||
# Sort categories alphabetically
|
||||
sorted_categorized = {
|
||||
k: categorized[k] for k in sorted(categorized.keys(), key=str.lower)
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
'categorized': sorted_categorized,
|
||||
'categorized': categorized,
|
||||
'uncategorized': uncategorized,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ portfolio_menu_categories:
|
||||
- ollama
|
||||
- openwebui
|
||||
- flowise
|
||||
- minio
|
||||
- qdrant
|
||||
- litellm
|
||||
|
||||
@@ -101,12 +102,14 @@ portfolio_menu_categories:
|
||||
- fusiondirectory
|
||||
- user-management
|
||||
|
||||
Customer Relationship:
|
||||
description: "Customer Relationship Management (CRM) software for managing customer relationships, sales pipelines, marketing, and support activities."
|
||||
Customer Relationship Management:
|
||||
description: "Tools for managing customer relationships, sales pipelines, marketing, and support activities."
|
||||
icon: "fa-solid fa-address-book"
|
||||
tags:
|
||||
- crm
|
||||
- customer
|
||||
- relationship
|
||||
- sales
|
||||
- marketing
|
||||
- support
|
||||
- espocrm
|
||||
@@ -219,7 +222,7 @@ portfolio_menu_categories:
|
||||
- snipe-it
|
||||
|
||||
Content Management:
|
||||
description: "Content Management Systems (CMS) and web publishing platforms"
|
||||
description: "CMS and web publishing platforms"
|
||||
icon: "fa-solid fa-file-alt"
|
||||
tags:
|
||||
- cms
|
||||
@@ -228,27 +231,4 @@ portfolio_menu_categories:
|
||||
- website
|
||||
- joomla
|
||||
- wordpress
|
||||
- blog
|
||||
|
||||
Commerce:
|
||||
description: "Platforms for building and managing online shops, product catalogs, and digital sales channels — including payment, inventory, and customer features."
|
||||
icon: "fa-solid fa-cart-shopping"
|
||||
tags:
|
||||
- commerce
|
||||
- ecommerce
|
||||
- shopware
|
||||
- shop
|
||||
- sales
|
||||
- store
|
||||
- magento
|
||||
- pretix
|
||||
|
||||
Storage:
|
||||
description: "High-performance, self-hosted storage solutions for managing, scaling, and accessing unstructured data — including object storage compatible with Amazon S3 APIs."
|
||||
icon: "fa-solid fa-database"
|
||||
tags:
|
||||
- storage
|
||||
- object-storage
|
||||
- s3
|
||||
- minio
|
||||
- datasets
|
||||
- blog
|
||||
29
roles/web-app-drupal/Administration.md
Normal file
29
roles/web-app-drupal/Administration.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Administration
|
||||
|
||||
## Shell access
|
||||
|
||||
```bash
|
||||
docker-compose exec -it application /bin/bash
|
||||
```
|
||||
|
||||
## Drush (inside the container)
|
||||
|
||||
```bash
|
||||
drush --version
|
||||
drush cr # Cache rebuild
|
||||
drush status # Site status
|
||||
drush cim -y # Config import (if using config sync)
|
||||
drush updb -y # Run DB updates
|
||||
```
|
||||
|
||||
## Database access (local DB service)
|
||||
|
||||
```bash
|
||||
docker-compose exec -it database /bin/mysql -u drupal -p
|
||||
```
|
||||
|
||||
## Test Email
|
||||
|
||||
```bash
|
||||
docker-compose exec -it application /bin/bash -lc 'echo "Test Email" | sendmail -v your-email@example.com'
|
||||
```
|
||||
32
roles/web-app-drupal/README.md
Normal file
32
roles/web-app-drupal/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Drupal
|
||||
|
||||
## Description
|
||||
|
||||
[Drupal](https://www.drupal.org/) is a powerful open-source CMS for building secure, extensible, and content-rich digital experiences.
|
||||
This role deploys a containerized **Drupal 10/11** instance optimized for production, including **msmtp** for outbound email, **Drush** for CLI administration, and **OpenID Connect (OIDC)** for SSO (e.g., Keycloak, Auth0, Azure AD).
|
||||
|
||||
## Overview
|
||||
|
||||
* **Flexible Content Model:** Entities, fields, and views for complex data needs.
|
||||
* **Security & Roles:** Fine-grained access control and active security team.
|
||||
* **Robust Ecosystem:** Thousands of modules and themes.
|
||||
* **CLI Automation:** Drush for installs, updates, and configuration import.
|
||||
* **OIDC SSO:** First-class login via external Identity Providers.
|
||||
|
||||
This automated Docker Compose deployment builds a custom Drupal image with Drush and msmtp, wires database credentials and config overrides via environment, and applies OIDC configuration via Ansible/Drush.
|
||||
|
||||
## OIDC
|
||||
|
||||
This role enables **OpenID Connect** via the `openid_connect` module and configures a **client entity** (e.g., `keycloak`) including endpoints and scopes. Global OIDC behavior (auto-create, link existing users, privacy) is set via `openid_connect.settings`.
|
||||
|
||||
## Further Resources
|
||||
|
||||
* [Drupal.org](https://www.drupal.org/)
|
||||
* [OpenID Connect module](https://www.drupal.org/project/openid_connect)
|
||||
|
||||
## Credits
|
||||
|
||||
Developed and maintained by **Kevin Veen-Birkenbach**
|
||||
Learn more at [veen.world](https://veen.world)
|
||||
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code)
|
||||
License: [Infinito.Nexus NonCommercial License](https://s.infinito.nexus/license)
|
||||
37
roles/web-app-drupal/config/main.yml
Normal file
37
roles/web-app-drupal/config/main.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
title: "Site"
|
||||
max_upload_size: "256M"
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
desktop: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
logout: true
|
||||
server:
|
||||
csp:
|
||||
flags: {}
|
||||
whitelist: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "drupal.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true
|
||||
drupal:
|
||||
version: latest
|
||||
image: drupal
|
||||
name: drupal
|
||||
backup:
|
||||
no_stop_required: true
|
||||
volumes:
|
||||
data: drupal_data
|
||||
rbac:
|
||||
roles:
|
||||
authenticated:
|
||||
description: "Logged-in user"
|
||||
content_editor:
|
||||
description: "Can create and edit content"
|
||||
site_admin:
|
||||
description: "Full site administration"
|
||||
23
roles/web-app-drupal/meta/main.yml
Normal file
23
roles/web-app-drupal/meta/main.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: >
|
||||
Drupal CMS in Docker with Drush, msmtp, and OpenID Connect (OIDC) SSO.
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- drupal
|
||||
- docker
|
||||
- cms
|
||||
- oidc
|
||||
- sso
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://docs.infinito.nexus"
|
||||
logo:
|
||||
class: "fa-solid fa-droplet"
|
||||
run_after:
|
||||
- web-app-keycloak
|
||||
9
roles/web-app-drupal/schema/main.yml
Normal file
9
roles/web-app-drupal/schema/main.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
credentials:
|
||||
administrator_password:
|
||||
description: "Initial password for the Drupal admin account"
|
||||
algorithm: "sha256"
|
||||
validation: "^[a-f0-9]{64}$"
|
||||
hash_salt:
|
||||
description: "Drupal hash_salt value used for one-time logins, CSRF tokens, etc."
|
||||
algorithm: "sha256"
|
||||
validation: "^[a-f0-9]{64}$"
|
||||
25
roles/web-app-drupal/tasks/01_settings_local_include.yml
Normal file
25
roles/web-app-drupal/tasks/01_settings_local_include.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
- name: "Ensure settings.php exists and includes settings.local.php"
|
||||
command: >
|
||||
docker exec -u root {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"set -e;
|
||||
f='{{ DRUPAL_DOCKER_CONF_PATH }}/settings.php';
|
||||
df='{{ DRUPAL_DOCKER_CONF_PATH }}/default.settings.php';
|
||||
if [ ! -f \"$f\" ] && [ -f \"$df\" ]; then
|
||||
cp \"$df\" \"$f\";
|
||||
chown www-data:www-data \"$f\";
|
||||
chmod 644 \"$f\";
|
||||
fi;
|
||||
php -r '
|
||||
$f=\"{{ DRUPAL_DOCKER_CONF_PATH }}/settings.php\";
|
||||
if (!file_exists($f)) { exit(0); }
|
||||
$c=file_get_contents($f);
|
||||
$inc=\"\\nif (file_exists(\\\"\$app_root/\$site_path/settings.local.php\\\")) { include \$app_root/\$site_path/settings.local.php; }\\n\";
|
||||
if (strpos($c, \"settings.local.php\") === false) {
|
||||
file_put_contents($f, $c.$inc);
|
||||
echo \"patched\";
|
||||
} else {
|
||||
echo \"exists\";
|
||||
}
|
||||
'"
|
||||
register: settings_local_include
|
||||
changed_when: "'patched' in settings_local_include.stdout"
|
||||
15
roles/web-app-drupal/tasks/02_install.yml
Normal file
15
roles/web-app-drupal/tasks/02_install.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
- name: "Run Drupal site:install via Drush"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} si standard -y
|
||||
--site-name='{{ applications | get_app_conf(application_id, 'title', True) }}'
|
||||
--account-name='{{ applications | get_app_conf(application_id, 'users.administrator.username') }}'
|
||||
--account-mail='{{ applications | get_app_conf(application_id, 'users.administrator.email', True) }}'
|
||||
--account-pass='{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}'
|
||||
--uri='{{ DRUPAL_URL }}'"
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
register: drupal_install
|
||||
changed_when: "'Installation complete' in drupal_install.stdout"
|
||||
failed_when: false
|
||||
12
roles/web-app-drupal/tasks/03_enable_modules.yml
Normal file
12
roles/web-app-drupal/tasks/03_enable_modules.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
- name: "Enable OpenID Connect core module"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} en openid_connect -y"
|
||||
changed_when: true
|
||||
|
||||
- name: "Enable OpenID Connect Keycloak preset (submodule of openid_connect)"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} en openid_connect_client_keycloak -y"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
59
roles/web-app-drupal/tasks/04_configure_oidc.yml
Normal file
59
roles/web-app-drupal/tasks/04_configure_oidc.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
- name: "Load OIDC vars"
|
||||
include_vars:
|
||||
file: "{{ role_path }}/vars/oidc.yml"
|
||||
name: oidc_vars
|
||||
|
||||
- name: "Apply openid_connect.settings (global)"
|
||||
loop: "{{ oidc_vars.oidc_settings | dict2items }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} cset -y
|
||||
openid_connect.settings {{ item.key }}
|
||||
{{ (item.value | to_json) if item.value is mapping or item.value is sequence else item.value }}"
|
||||
|
||||
- name: "Ensure OIDC client entity exists"
|
||||
vars:
|
||||
client_id: "{{ oidc_vars.oidc_client.id }}"
|
||||
client_label: "{{ oidc_vars.oidc_client.label }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} eval '
|
||||
$id=\"{{ client_id }}\"; $label=\"{{ client_label }}\";
|
||||
$storage=\Drupal::entityTypeManager()->getStorage(\"openid_connect_client\");
|
||||
if (!$storage->load($id)) {
|
||||
$client=$storage->create([\"id\"=>$id,\"label\"=>$label]);
|
||||
$client->save();
|
||||
print \"created\";
|
||||
} else { print \"exists\"; }'"
|
||||
register: client_exists
|
||||
changed_when: "'created' in client_exists.stdout"
|
||||
|
||||
- name: "Apply OIDC client settings"
|
||||
vars:
|
||||
client_id: "{{ oidc_vars.oidc_client.id }}"
|
||||
settings_map: "{{ oidc_vars.oidc_client.settings }}"
|
||||
kv: "{{ settings_map | dict2items }}"
|
||||
loop: "{{ kv }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} eval '
|
||||
$id=\"{{ client_id }}\";
|
||||
$key=\"{{ item.key }}\";
|
||||
$val=json_decode(base64_decode(\"{{ (item.value | to_json | b64encode) }}\"), true);
|
||||
$storage=\Drupal::entityTypeManager()->getStorage(\"openid_connect_client\");
|
||||
$c=$storage->load($id);
|
||||
$s=$c->get(\"settings\");
|
||||
$s[$key]=$val;
|
||||
$c->set(\"settings\", $s);
|
||||
$c->save();'"
|
||||
changed_when: true
|
||||
|
||||
- name: "Clear caches after OIDC config"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/var/www/html/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} cr"
|
||||
changed_when: false
|
||||
19
roles/web-app-drupal/tasks/05_trusted_hosts.yml
Normal file
19
roles/web-app-drupal/tasks/05_trusted_hosts.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
- name: "Set trusted_host_patterns for canonical domains"
|
||||
vars:
|
||||
patterns: "{{ DRUPAL_DOMAINS
|
||||
| map('regex_replace','\\\\.','\\\\\\\\.')
|
||||
| map('regex_replace','^','^')
|
||||
| map('regex_replace','$','$')
|
||||
| list }}"
|
||||
php_array: "{{ patterns | to_json }}"
|
||||
command: >
|
||||
docker exec -u root {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"php -r '
|
||||
$f="{{ DRUPAL_DOCKER_CONF_PATH }}/settings.local.php";
|
||||
$c=file_exists($f)?file_get_contents($f):"<?php\n";
|
||||
// Remove existing assignment of $settings[\"trusted_host_patterns\"] (if any)
|
||||
$c=preg_replace(\"/(\\\\$settings\\['trusted_host_patterns'\\]\\s*=).*?;/s\", \"\", $c);
|
||||
$c.="\n\$settings[\'trusted_host_patterns\'] = ".var_export(json_decode("{{ php_array|e }}", true), true).";\n";
|
||||
file_put_contents($f,$c);
|
||||
'"
|
||||
changed_when: true
|
||||
55
roles/web-app-drupal/tasks/main.yml
Normal file
55
roles/web-app-drupal/tasks/main.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
- name: "Include role sys-stk-front-proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-front-proxy
|
||||
loop: "{{ DRUPAL_DOMAINS }}"
|
||||
loop_control:
|
||||
loop_var: domain
|
||||
vars:
|
||||
proxy_extra_configuration: "client_max_body_size {{ DRUPAL_MAX_UPLOAD_SIZE }};"
|
||||
http_port: "{{ ports.localhost.http[application_id] }}"
|
||||
|
||||
- name: "Load docker and DB for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-back-stateful
|
||||
vars:
|
||||
docker_compose_flush_handlers: false
|
||||
|
||||
- name: "Transfer upload.ini to {{ DRUPAL_CONFIG_UPLOAD_ABS }}"
|
||||
template:
|
||||
src: upload.ini.j2
|
||||
dest: "{{ DRUPAL_CONFIG_UPLOAD_ABS }}"
|
||||
notify:
|
||||
- docker compose up
|
||||
- docker compose build
|
||||
|
||||
- name: "Transfer msmtprc to {{ DRUPAL_MSMTP_ABS }}"
|
||||
template:
|
||||
src: "{{ DRUPAL_MSMTP_SRC }}"
|
||||
dest: "{{ DRUPAL_MSMTP_ABS }}"
|
||||
notify: docker compose up
|
||||
|
||||
- name: "Transfer settings.local.php overrides"
|
||||
template:
|
||||
src: settings.local.php.j2
|
||||
dest: "{{ DRUPAL_SETTINGS_LOCAL_ABS }}"
|
||||
notify: docker compose up
|
||||
|
||||
- name: Flush handlers to make container ready
|
||||
meta: flush_handlers
|
||||
|
||||
- name: "Ensure settings.php includes settings.local.php"
|
||||
include_tasks: 01_settings_local_include.yml
|
||||
|
||||
- name: "Install Drupal (site:install)"
|
||||
include_tasks: 02_install.yml
|
||||
|
||||
- name: "Enable OIDC modules"
|
||||
include_tasks: 03_enable_modules.yml
|
||||
when: applications | get_app_conf(application_id, 'features.oidc')
|
||||
|
||||
- name: "Configure OIDC (global + client)"
|
||||
include_tasks: 04_configure_oidc.yml
|
||||
when: applications | get_app_conf(application_id, 'features.oidc')
|
||||
|
||||
- name: "Harden trusted host patterns"
|
||||
include_tasks: 05_trusted_hosts.yml
|
||||
75
roles/web-app-drupal/templates/Dockerfile.j2
Normal file
75
roles/web-app-drupal/templates/Dockerfile.j2
Normal file
@@ -0,0 +1,75 @@
|
||||
FROM {{ DRUPAL_IMAGE }}:{{ DRUPAL_VERSION }}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# System dependencies (mail support + basic tools)
|
||||
# -------------------------------------------------------------------
|
||||
RUN apt-get update && \
|
||||
apt-get install -y msmtp msmtp-mta git unzip zip less nano curl vim && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Install Composer
|
||||
# -------------------------------------------------------------------
|
||||
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
|
||||
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
|
||||
&& rm composer-setup.php
|
||||
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Build Drupal project with Drush + OpenID Connect
|
||||
# IMPORTANT:
|
||||
# - The Drupal base image uses /var/www/html as a symlink to /opt/drupal/web
|
||||
# - Therefore, the actual project root must be placed in /opt/drupal
|
||||
# -------------------------------------------------------------------
|
||||
RUN set -eux; \
|
||||
builddir="$(mktemp -d)"; \
|
||||
composer create-project --no-interaction --no-ansi --no-progress drupal/recommended-project:^10 "$builddir"; \
|
||||
composer --working-dir="$builddir" require -n drush/drush:^13 drupal/openid_connect:^1; \
|
||||
rm -rf /opt/drupal/* /opt/drupal/.[!.]* /opt/drupal/..?* 2>/dev/null || true; \
|
||||
mkdir -p /opt/drupal; \
|
||||
cp -a "$builddir"/. /opt/drupal/; \
|
||||
rm -rf "$builddir"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Make vendor binaries available in PATH
|
||||
# -------------------------------------------------------------------
|
||||
ENV PATH="/opt/drupal/vendor/bin:${PATH}"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# PHP upload configuration
|
||||
# -------------------------------------------------------------------
|
||||
COPY {{ DRUPAL_CONFIG_UPLOAD_REL }} $PHP_INI_DIR/conf.d/
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Permissions and ownership fixes
|
||||
# -------------------------------------------------------------------
|
||||
RUN set -eux; \
|
||||
# Ensure all directories are traversable
|
||||
chmod 755 /var /var/www /opt /opt/drupal; \
|
||||
# Ensure correct ownership for Drupal files
|
||||
chown -R www-data:www-data /opt/drupal; \
|
||||
# Apply default permissions
|
||||
find /opt/drupal -type d -exec chmod 755 {} +; \
|
||||
find /opt/drupal -type f -exec chmod 644 {} +; \
|
||||
# Ensure vendor binaries are executable
|
||||
if [ -d /opt/drupal/vendor/bin ]; then chmod a+rx /opt/drupal/vendor/bin/*; fi; \
|
||||
if [ -f /opt/drupal/vendor/drush/drush/drush ]; then chmod a+rx /opt/drupal/vendor/drush/drush/drush; fi; \
|
||||
# Ensure the docroot (/opt/drupal/web) is accessible
|
||||
if [ -d /opt/drupal/web ]; then \
|
||||
chmod 755 /opt/drupal/web; \
|
||||
find /opt/drupal/web -type d -exec chmod 755 {} +; \
|
||||
fi; \
|
||||
# Ensure settings.local.php exists and is owned by www-data
|
||||
install -o www-data -g www-data -m 640 /dev/null /opt/drupal/web/sites/default/settings.local.php
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Runtime defaults
|
||||
# -------------------------------------------------------------------
|
||||
USER www-data
|
||||
WORKDIR /var/www/html # symlink pointing to /opt/drupal/web
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Build-time check (optional)
|
||||
# -------------------------------------------------------------------
|
||||
RUN drush --version
|
||||
22
roles/web-app-drupal/templates/docker-compose.yml.j2
Normal file
22
roles/web-app-drupal/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,22 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
application:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
image: {{ DRUPAL_CUSTOM_IMAGE }}
|
||||
container_name: {{ DRUPAL_CONTAINER }}
|
||||
{{ lookup('template', 'roles/docker-container/templates/build.yml.j2') | indent(4) }}
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:80"
|
||||
volumes:
|
||||
- data:/var/www/html/web/sites/default/files
|
||||
- {{ DRUPAL_MSMTP_ABS }}:/etc/msmtprc
|
||||
- {{ DRUPAL_SETTINGS_LOCAL_ABS }}:{{ DRUPAL_DOCKER_CONF_PATH }}/settings.local.php
|
||||
|
||||
{% include 'roles/docker-container/templates/healthcheck/msmtp_curl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: "{{ DRUPAL_VOLUME }}"
|
||||
7
roles/web-app-drupal/templates/env.j2
Normal file
7
roles/web-app-drupal/templates/env.j2
Normal file
@@ -0,0 +1,7 @@
|
||||
DRUPAL_DB_HOST= "{{ database_host }}:{{ database_port }}"
|
||||
DRUPAL_DB_USER= "{{ database_username }}"
|
||||
DRUPAL_DB_PASSWORD= "{{ database_password }}"
|
||||
DRUPAL_DB_NAME= "{{ database_name }}"
|
||||
|
||||
# Debug flags (optional)
|
||||
DRUPAL_DEBUG={{ MODE_DEBUG | lower }}
|
||||
49
roles/web-app-drupal/templates/settings.local.php.j2
Normal file
49
roles/web-app-drupal/templates/settings.local.php.j2
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Local settings overrides generated by Ansible.
|
||||
* - Reads DB + OIDC endpoints from environment variables.
|
||||
* - Sets $databases and selected $config overrides.
|
||||
*/
|
||||
|
||||
$env = getenv();
|
||||
|
||||
/** Database **/
|
||||
$host = getenv('DRUPAL_DB_HOST') ?: '{{ database_host }}:{{ database_port }}';
|
||||
$db = getenv('DRUPAL_DB_NAME') ?: '{{ database_name }}';
|
||||
$user = getenv('DRUPAL_DB_USER') ?: '{{ database_username }}';
|
||||
$pass = getenv('DRUPAL_DB_PASSWORD') ?: '{{ database_password }}';
|
||||
|
||||
$parts = explode(':', $host, 2);
|
||||
$hostname = $parts[0];
|
||||
$port = isset($parts[1]) ? (int)$parts[1] : 3306;
|
||||
|
||||
$databases['default']['default'] = [
|
||||
'database' => $db,
|
||||
'username' => $user,
|
||||
'password' => $pass,
|
||||
'prefix' => '',
|
||||
'host' => $hostname,
|
||||
'port' => $port,
|
||||
'namespace'=> 'Drupal\\Core\\Database\\Driver\\mysql',
|
||||
'driver' => 'mysql',
|
||||
];
|
||||
|
||||
/** OIDC endpoint hints (optional) — the real config is applied via Drush. */
|
||||
$config['openid_connect.settings']['automatic_account_creation'] = true;
|
||||
$config['openid_connect.settings']['always_save_userinfo'] = true;
|
||||
$config['openid_connect.settings']['link_existing_users'] = true;
|
||||
|
||||
/** Trusted host patterns can be extended by Ansible task 04_trusted_hosts.yml */
|
||||
|
||||
/** Enable local services YML if present */
|
||||
$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.local.yml';
|
||||
|
||||
// Reverse proxy optional über ENV setzen (z.B. "10.0.0.0/8, 172.16.0.0/12")
|
||||
$proxy = getenv('REVERSE_PROXY_ADDRESSES');
|
||||
if ($proxy) {
|
||||
$settings['reverse_proxy'] = TRUE;
|
||||
$settings['reverse_proxy_addresses'] = array_map('trim', explode(',', $proxy));
|
||||
}
|
||||
|
||||
/** Hash salt (from schema/credentials, hashed with SHA-256) */
|
||||
$settings['hash_salt'] = '{{ applications | get_app_conf(application_id, "credentials.hash_salt", True) }}';
|
||||
8
roles/web-app-drupal/templates/upload.ini.j2
Normal file
8
roles/web-app-drupal/templates/upload.ini.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
file_uploads = On
|
||||
memory_limit = {{ DRUPAL_MAX_UPLOAD_SIZE }}
|
||||
upload_max_filesize = {{ DRUPAL_MAX_UPLOAD_SIZE }}
|
||||
post_max_size = {{ DRUPAL_MAX_UPLOAD_SIZE }}
|
||||
max_execution_time = 300
|
||||
|
||||
; Use msmtp as the Mail Transfer Agent
|
||||
sendmail_path = "/usr/bin/msmtp -t"
|
||||
4
roles/web-app-drupal/users/main.yml
Normal file
4
roles/web-app-drupal/users/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
||||
email: "administrator@{{ PRIMARY_DOMAIN }}"
|
||||
28
roles/web-app-drupal/vars/main.yml
Normal file
28
roles/web-app-drupal/vars/main.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
# General
|
||||
|
||||
application_id: "web-app-drupal"
|
||||
database_type: "mariadb"
|
||||
|
||||
# Drupal
|
||||
|
||||
DRUPAL_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
DRUPAL_CUSTOM_IMAGE: "drupal_custom"
|
||||
DRUPAL_DOCKER_HTML_PATH: "/var/www/html"
|
||||
DRUPAL_DOCKER_CONF_PATH: "/var/www/html/sites/default"
|
||||
DRUPAL_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.drupal.version') }}"
|
||||
DRUPAL_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.drupal.image') }}"
|
||||
DRUPAL_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.drupal.name') }}"
|
||||
DRUPAL_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
DRUPAL_DOMAINS: "{{ applications | get_app_conf(application_id, 'server.domains.canonical') }}"
|
||||
DRUPAL_USER: "www-data"
|
||||
|
||||
DRUPAL_CONFIG_UPLOAD_REL: "config/upload.ini"
|
||||
DRUPAL_CONFIG_UPLOAD_ABS: "{{ [docker_compose.directories.instance, DRUPAL_CONFIG_UPLOAD_REL] | path_join }}"
|
||||
|
||||
DRUPAL_SETTINGS_LOCAL_REL: "config/settings.local.php"
|
||||
DRUPAL_SETTINGS_LOCAL_ABS: "{{ [docker_compose.directories.instance, DRUPAL_SETTINGS_LOCAL_REL] | path_join }}"
|
||||
|
||||
DRUPAL_MSMTP_SRC: "{{ [ playbook_dir, 'roles/sys-svc-msmtp/templates/msmtprc.conf.j2' ] | path_join }}"
|
||||
DRUPAL_MSMTP_ABS: "{{ [ docker_compose.directories.config, 'msmtprc.conf'] | path_join }}"
|
||||
|
||||
DRUPAL_MAX_UPLOAD_SIZE: "{{ applications | get_app_conf(application_id, 'max_upload_size') }}"
|
||||
33
roles/web-app-drupal/vars/oidc.yml
Normal file
33
roles/web-app-drupal/vars/oidc.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
# OIDC configuration for Drupal's OpenID Connect module.
|
||||
|
||||
# Global settings for openid_connect.settings
|
||||
|
||||
oidc_settings:
|
||||
automatic_account_creation: true # Auto-create users on first login
|
||||
always_save_userinfo: true # Store latest userinfo on each login
|
||||
link_existing_users: true # Match existing users by email
|
||||
login_display: "button" # 'button' or 'form'
|
||||
enforced: false # If true, require login for the whole site
|
||||
|
||||
# OIDC client entity (e.g., 'keycloak')
|
||||
|
||||
oidc_client:
|
||||
id: "keycloak"
|
||||
label: "Keycloak"
|
||||
settings:
|
||||
client_id: "{{ OIDC.CLIENT.ID }}"
|
||||
client_secret: "{{ OIDC.CLIENT.SECRET }}"
|
||||
authorization_endpoint: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
token_endpoint: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
userinfo_endpoint: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
end_session_endpoint: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
scopes:
|
||||
- "openid"
|
||||
- "email"
|
||||
- "profile"
|
||||
use_standard_claims: true
|
||||
# Optional claim mapping examples:
|
||||
# username_claim: "{{ OIDC.ATTRIBUTES.USERNAME }}"
|
||||
# email_claim: "{{ OIDC.ATTRIBUTES.EMAIL }}"
|
||||
# given_name_claim: "{{ OIDC.ATTRIBUTES.GIVEN_NAME }}"
|
||||
# family_name_claim: "{{ OIDC.ATTRIBUTES.FAMILY_NAME }}"
|
||||
@@ -1,34 +0,0 @@
|
||||
# Shopware
|
||||
|
||||
## Description
|
||||
|
||||
Empower your e-commerce vision with **Shopware 6**, a modern, flexible, and open-source commerce platform built on **Symfony and Vue.js**. Designed for growth and innovation, it enables seamless integration, outstanding customer experiences, and complete control over your digital business. Build, scale, and sell with confidence.
|
||||
|
||||
## Overview
|
||||
|
||||
This role deploys **Shopware 6** using **Docker**. It automates installation, migration, and configuration of your storefront, integrating with a central **MariaDB** database.
|
||||
Optional components like **Redis** and **OpenSearch** enhance performance and search capabilities, while **OIDC** and **LDAP** support integration with centralized identity systems such as **Keycloak**.
|
||||
|
||||
With automated setup, update handling, variable management, and plugin-based authentication, this role simplifies the deployment and maintenance of your Shopware instance.
|
||||
|
||||
## Features
|
||||
|
||||
* **Modern and Scalable:** A robust Symfony-based framework optimized for commerce innovation.
|
||||
* **Automated Setup & Maintenance:** Installs, migrates, and configures Shopware automatically.
|
||||
* **Extensible Architecture:** Optional Redis, OpenSearch, and plugin-based IAM integrations.
|
||||
* **Centralized Database Access:** Connects seamlessly to the shared MariaDB service.
|
||||
* **Integrated Configuration:** Environment and Docker Compose variables managed automatically.
|
||||
|
||||
## Further Resources
|
||||
|
||||
* [Shopware Official Website](https://www.shopware.com/en/)
|
||||
* [Shopware Developer Documentation](https://developer.shopware.com/)
|
||||
* [Shopware Store (Plugins)](https://store.shopware.com/en/)
|
||||
|
||||
## Credits
|
||||
|
||||
Developed and maintained by **Kevin Veen-Birkenbach**.
|
||||
Learn more at [veen.world](https://www.veen.world).
|
||||
|
||||
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code)
|
||||
Licensed under [Infinito.Nexus NonCommercial License](https://s.infinito.nexus/license).
|
||||
@@ -1,3 +0,0 @@
|
||||
# to-dos
|
||||
- Implement OIDC
|
||||
- Implement LDAP
|
||||
@@ -1,75 +0,0 @@
|
||||
title: "{{ SOFTWARE_NAME }} Shop"
|
||||
features:
|
||||
central_database: true
|
||||
redis: true
|
||||
ldap: false # Not implemented yet
|
||||
oidc: false # Not implemented yet
|
||||
logout: true
|
||||
server:
|
||||
csp:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
whitelist: {}
|
||||
domains:
|
||||
aliases: []
|
||||
canonical:
|
||||
- shop.{{ PRIMARY_DOMAIN }}
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true
|
||||
|
||||
init:
|
||||
name: software-init
|
||||
cpus: 1.0
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
|
||||
|
||||
# Base PHP image used by all app services
|
||||
shopware:
|
||||
image: "ghcr.io/shopware/docker-base"
|
||||
version: "8.3"
|
||||
|
||||
web:
|
||||
name: "shopware-web"
|
||||
port: 8000
|
||||
cpus: 1.0
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
|
||||
worker:
|
||||
name: "shopware-worker"
|
||||
entrypoint: [ "php", "bin/console", "messenger:consume", "async", "low_priority", "--time-limit=300", "--memory-limit=512M" ]
|
||||
replicas: 3
|
||||
cpus: 1.0
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
|
||||
scheduler:
|
||||
name: "shopware-scheduler"
|
||||
entrypoint: [ "php", "bin/console", "scheduled-task:run" ]
|
||||
cpus: 0.5
|
||||
mem_reservation: 512m
|
||||
mem_limit: 1g
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
image: "redis"
|
||||
version: "7-alpine"
|
||||
cpus: 0.25
|
||||
mem_reservation: 256m
|
||||
mem_limit: 512m
|
||||
|
||||
opensearch:
|
||||
enabled: true
|
||||
image: "opensearchproject/opensearch"
|
||||
version: "2.12.0"
|
||||
name: "shopware-opensearch"
|
||||
cpus: 1.0
|
||||
mem_reservation: 2g
|
||||
mem_limit: 4g
|
||||
|
||||
volumes:
|
||||
data: "shopware_data"
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Paths / constants
|
||||
APP_ROOT="/var/www/html"
|
||||
MARKER="$APP_ROOT/.infinito/installed"
|
||||
|
||||
cd "$APP_ROOT"
|
||||
mkdir -p "$APP_ROOT/.infinito"
|
||||
|
||||
echo "[INIT] Checking database via PDO..."
|
||||
php -r '
|
||||
$url = getenv("DATABASE_URL");
|
||||
if (!$url) { fwrite(STDERR, "DATABASE_URL not set\n"); exit(1); }
|
||||
$p = parse_url($url);
|
||||
if (!$p || !isset($p["scheme"])) { fwrite(STDERR, "Invalid DATABASE_URL\n"); exit(1); }
|
||||
$scheme = $p["scheme"];
|
||||
if ($scheme === "mysql" || $scheme === "mariadb") {
|
||||
$host = $p["host"] ?? "localhost";
|
||||
$port = $p["port"] ?? 3306;
|
||||
$db = ltrim($p["path"] ?? "", "/");
|
||||
$user = $p["user"] ?? "";
|
||||
$pass = $p["pass"] ?? "";
|
||||
$dsn = "mysql:host=".$host.";port=".$port.";dbname=".$db.";charset=utf8mb4";
|
||||
} else {
|
||||
fwrite(STDERR, "Unsupported DB scheme: ".$scheme."\n"); exit(1);
|
||||
}
|
||||
$retries = 60;
|
||||
while ($retries-- > 0) {
|
||||
try { $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_TIMEOUT => 3]); exit(0); }
|
||||
catch (Exception $e) { sleep(2); }
|
||||
}
|
||||
fwrite(STDERR, "DB not reachable\n"); exit(1);
|
||||
'
|
||||
|
||||
if [ ! -f "$MARKER" ]; then
|
||||
echo "[INIT] Checking if database is empty..."
|
||||
# PHP exits: 0 = empty, 100 = non-empty, 1 = error
|
||||
if php -r '
|
||||
$url = getenv("DATABASE_URL");
|
||||
$p = parse_url($url);
|
||||
$db = ltrim($p["path"] ?? "", "/");
|
||||
$dsn = "mysql:host=".($p["host"]??"localhost").";port=".($p["port"]??3306).";dbname=".$db.";charset=utf8mb4";
|
||||
try {
|
||||
$pdo = new PDO($dsn, $p["user"] ?? "", $p["pass"] ?? "");
|
||||
$q = $pdo->query("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=".$pdo->quote($db));
|
||||
$cnt = (int)$q->fetchColumn();
|
||||
if ($cnt === 0) { exit(0); } else { exit(100); }
|
||||
} catch (Exception $e) { fwrite(STDERR, $e->getMessage()."\n"); exit(1); }
|
||||
'; then
|
||||
DBCHK=0
|
||||
else
|
||||
DBCHK=$?
|
||||
fi
|
||||
|
||||
if [ "$DBCHK" -eq 0 ]; then
|
||||
echo "[INIT] Installing Shopware (empty DB detected)..."
|
||||
# IMPORTANT: no --force; let Shopware run its internal steps only on empty DB
|
||||
php -d memory_limit=1024M bin/console system:install --basic-setup --create-database
|
||||
elif [ "$DBCHK" -eq 100 ]; then
|
||||
echo "[INIT] Database is not empty -> skipping system:install"
|
||||
else
|
||||
echo "[INIT] Database check failed (code $DBCHK)"; exit 1
|
||||
fi
|
||||
|
||||
# Safe to run (no-ops when up-to-date)
|
||||
php -d memory_limit=1024M bin/console database:migrate --all || true
|
||||
php -d memory_limit=1024M bin/console database:migrate-destructive --all || true
|
||||
|
||||
# Housekeeping
|
||||
php bin/console cache:clear || true
|
||||
php bin/console dal:refresh:index || true
|
||||
|
||||
# Marker + perms
|
||||
touch "$MARKER"
|
||||
chown -R www-data:www-data "$APP_ROOT"
|
||||
|
||||
echo "[INIT] Done."
|
||||
else
|
||||
echo "[INIT] Marker found, skipping install."
|
||||
fi
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Shopware is a modern open-source eCommerce platform built on PHP and Symfony. It enables businesses to create scalable online stores with flexible product management, intuitive administration, customizable storefronts, and powerful APIs for headless and omnichannel commerce."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- shopware
|
||||
- ecommerce
|
||||
repository: https://s.infinito.nexus/code
|
||||
issue_tracker_url: https://s.infinito.nexus/issues
|
||||
documentation: "https://docs.infinito.nexus/"
|
||||
logo:
|
||||
class: "fa-solid fa-cart-shopping"
|
||||
run_after:
|
||||
- web-app-keycloak
|
||||
- web-app-mailu
|
||||
dependencies: []
|
||||
@@ -1,2 +0,0 @@
|
||||
# Minimal schema placeholder (extend with your own config contract if desired)
|
||||
credentials: {}
|
||||
@@ -1,19 +0,0 @@
|
||||
# Ensures that the admin user exists and always has the desired password
|
||||
- name: "Ensure Shopware admin exists and has the desired password"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} sh -lc '
|
||||
set -e
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console user:create "{{ users.administrator.username }}" \
|
||||
--admin \
|
||||
--password="{{ users.administrator.password }}" \
|
||||
--firstName="{{ users.administrator.username }}" \
|
||||
--lastName="{{ PRIMARY_DOMAIN | lower }}" \
|
||||
--email="{{ users.administrator.email }}" || true
|
||||
php bin/console user:change-password "{{ users.administrator.username }}" \
|
||||
--password="{{ users.administrator.password }}" || true
|
||||
php bin/console user:update "{{ users.administrator.username }}" \
|
||||
--email="{{ users.administrator.email }}" 2>/dev/null || true
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
@@ -1,7 +0,0 @@
|
||||
- name: Install & configure OIDC plugin (if enabled)
|
||||
include_tasks: setup/oidc.yml
|
||||
when: applications | get_app_conf(application_id, 'features.oidc')
|
||||
|
||||
- name: Install & configure LDAP plugin (if enabled)
|
||||
include_tasks: setup/ldap.yml
|
||||
when: applications | get_app_conf(application_id, 'features.ldap')
|
||||
@@ -1,7 +0,0 @@
|
||||
- name: Remove OIDC plugin if disabled
|
||||
include_tasks: cleanup/oidc.yml
|
||||
when: not (applications | get_app_conf(application_id, 'features.oidc'))
|
||||
|
||||
- name: Remove LDAP plugin if disabled
|
||||
include_tasks: cleanup/ldap.yml
|
||||
when: not (applications | get_app_conf(application_id, 'features.ldap'))
|
||||
@@ -1,10 +0,0 @@
|
||||
- name: "Deactivate/uninstall LDAP plugin if present"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} bash -lc '
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console plugin:deactivate INFX_LDAP_PLUGIN || true
|
||||
php bin/console plugin:uninstall INFX_LDAP_PLUGIN --keep-user-data || true
|
||||
php bin/console cache:clear
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
@@ -1,10 +0,0 @@
|
||||
- name: "Deactivate/uninstall OIDC plugin if present"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} bash -lc '
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console plugin:deactivate INFX_OIDC_PLUGIN || true
|
||||
php bin/console plugin:uninstall INFX_OIDC_PLUGIN --keep-user-data || true
|
||||
php bin/console cache:clear
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
- name: "Load docker, DB and proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-full-stateful
|
||||
vars:
|
||||
docker_compose_flush_handlers: false
|
||||
|
||||
- name: "Deploy {{ SHOPWARE_INIT_HOST }}"
|
||||
copy:
|
||||
src: init.sh
|
||||
dest: "{{ SHOPWARE_INIT_HOST }}"
|
||||
mode: "0755"
|
||||
notify:
|
||||
- docker compose up
|
||||
- docker compose build
|
||||
|
||||
- name: "Flush docker compose handlers"
|
||||
meta: flush_handlers
|
||||
|
||||
- name: Wait for Shopware HTTP endpoint
|
||||
wait_for:
|
||||
host: "127.0.0.1"
|
||||
port: "{{ ports.localhost.http[application_id] }}"
|
||||
delay: 5
|
||||
timeout: 300
|
||||
|
||||
- name: "Ensure admin user exists with correct password"
|
||||
include_tasks: 01_admin.yml
|
||||
|
||||
#- name: Execute setup routines (OIDC/LDAP)
|
||||
# include_tasks: 02_setup.yml
|
||||
#
|
||||
#- name: Execute cleanup routines
|
||||
# include_tasks: 03_cleanup.yml
|
||||
# when: MODE_CLEANUP
|
||||
@@ -1,27 +0,0 @@
|
||||
# Replace INFX_LDAP_PLUGIN with the actual plugin name you use
|
||||
- name: "Install LDAP admin plugin & activate"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} bash -lc '
|
||||
set -e
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console plugin:refresh
|
||||
php bin/console plugin:install --activate INFX_LDAP_PLUGIN || true
|
||||
php bin/console cache:clear
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
|
||||
- name: "Configure LDAP connection"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} bash -lc '
|
||||
set -e
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console system:config:set "InfxLdap.config.host" "{{ LDAP.SERVER.DOMAIN }}"
|
||||
php bin/console system:config:set "InfxLdap.config.port" "{{ LDAP.SERVER.PORT }}"
|
||||
php bin/console system:config:set "InfxLdap.config.bindDn" "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
php bin/console system:config:set "InfxLdap.config.password" "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
php bin/console system:config:set "InfxLdap.config.userBase" "{{ LDAP.DN.OU.USERS }}"
|
||||
php bin/console cache:clear
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
@@ -1,26 +0,0 @@
|
||||
# Replace INFX_OIDC_PLUGIN with the actual plugin name (Composer or local)
|
||||
- name: "Install OIDC plugin & activate"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} bash -lc '
|
||||
set -e
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console plugin:refresh
|
||||
php bin/console plugin:install --activate INFX_OIDC_PLUGIN || true
|
||||
php bin/console cache:clear
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
|
||||
- name: "Configure OIDC via system:config"
|
||||
shell: |
|
||||
docker exec -i --user {{ SHOPWARE_USER }} {{ SHOPWARE_WEB_CONTAINER }} bash -lc '
|
||||
set -e
|
||||
cd {{ SHOPWARE_ROOT }}
|
||||
php bin/console system:config:set "InfxOidc.config.clientId" "{{ OIDC.CLIENT.ID }}"
|
||||
php bin/console system:config:set "InfxOidc.config.clientSecret" "{{ OIDC.CLIENT.SECRET }}"
|
||||
php bin/console system:config:set "InfxOidc.config.discoveryUrl" "{{ OIDC.CLIENT.DISCOVERY_DOCUMENT }}"
|
||||
php bin/console system:config:set "InfxOidc.config.scopes" "openid profile email"
|
||||
php bin/console cache:clear
|
||||
'
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
@@ -1,92 +0,0 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Shopware Application Image (Alpine-compatible)
|
||||
# ------------------------------------------------------------------------------
|
||||
# - Stage 1 (builder): use Composer to fetch Shopware while ignoring build-time
|
||||
# PHP extensions (we'll install them in the runtime image).
|
||||
# - Stage 2 (runtime): install required PHP extensions and copy the app + init.sh
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
############################
|
||||
# Stage 1: Builder
|
||||
############################
|
||||
FROM composer:2.7 AS builder
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1 \
|
||||
COMPOSER_NO_INTERACTION=1 \
|
||||
COMPOSER_PROCESS_TIMEOUT=900
|
||||
|
||||
WORKDIR /app
|
||||
ARG SHOPWARE_PROD_VERSION=shopware/production:6.7.3.1
|
||||
|
||||
# 1) Scaffold project without installing dependencies
|
||||
RUN set -eux; \
|
||||
composer create-project "${SHOPWARE_PROD_VERSION}" /app --no-install
|
||||
|
||||
# 2) Install dependencies (ignoring build-time extension checks) + add Redis transport
|
||||
RUN set -eux; \
|
||||
composer install \
|
||||
--no-dev \
|
||||
--optimize-autoloader \
|
||||
--no-progress \
|
||||
--no-scripts \
|
||||
--ignore-platform-req=ext-gd \
|
||||
--ignore-platform-req=ext-intl \
|
||||
--ignore-platform-req=ext-pdo_mysql; \
|
||||
composer require symfony/redis-messenger:^6.4 \
|
||||
-W \
|
||||
--no-scripts \
|
||||
--no-progress \
|
||||
--update-no-dev \
|
||||
--ignore-platform-req=ext-gd \
|
||||
--ignore-platform-req=ext-intl \
|
||||
--ignore-platform-req=ext-pdo_mysql \
|
||||
--ignore-platform-req=ext-redis
|
||||
|
||||
|
||||
############################
|
||||
# Stage 2: Runtime
|
||||
############################
|
||||
FROM ghcr.io/shopware/docker-base:8.3
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install required PHP extensions in the Alpine-based runtime
|
||||
# (try php83-*, fall back to php82-*, then to generic)
|
||||
USER root
|
||||
RUN set -eux; \
|
||||
apk add --no-cache php83-gd || apk add --no-cache php82-gd || apk add --no-cache php-gd || true; \
|
||||
apk add --no-cache php83-intl || apk add --no-cache php82-intl || apk add --no-cache php-intl || true; \
|
||||
apk add --no-cache php83-pdo_mysql || apk add --no-cache php82-pdo_mysql || apk add --no-cache php-pdo_mysql || true; \
|
||||
apk add --no-cache php83-redis || apk add --no-cache php82-redis || apk add --no-cache php-redis || true
|
||||
|
||||
# Copy built application from the builder
|
||||
COPY --chown=www-data:www-data --from=builder /app /var/www/html
|
||||
|
||||
# Optional: snapshot of pristine app to seed an empty volume (used by init container)
|
||||
RUN mkdir -p /usr/src/shopware \
|
||||
&& cp -a /var/www/html/. /usr/src/shopware/. \
|
||||
&& chown -R www-data:www-data /var/www/html /usr/src/shopware
|
||||
|
||||
# Ensure writable directories exist with correct ownership
|
||||
RUN set -eux; \
|
||||
mkdir -p \
|
||||
/var/www/html/files \
|
||||
/var/www/html/var \
|
||||
/var/www/html/public/media \
|
||||
/var/www/html/public/thumbnail \
|
||||
/var/www/html/public/sitemap \
|
||||
/var/www/html/public/theme; \
|
||||
chown -R www-data:www-data /var/www/html
|
||||
|
||||
# Add trusted proxies wiring (Symfony reads env TRUSTED_PROXIES)
|
||||
RUN set -eux; \
|
||||
mkdir -p /var/www/html/config/packages; \
|
||||
if [ ! -f /var/www/html/config/packages/framework.yaml ]; then \
|
||||
printf "framework:\n trusted_proxies: '%%env(TRUSTED_PROXIES)%%'\n" > /var/www/html/config/packages/framework.yaml; \
|
||||
fi
|
||||
|
||||
# Drop back to the app user
|
||||
USER www-data
|
||||
|
||||
# Expose internal port & add a lightweight healthcheck
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=5 --start-period=20s \
|
||||
CMD php -r '$s=@fsockopen("127.0.0.1", 8000, $e, $t, 3); if(!$s) exit(1); fclose($s);'
|
||||
@@ -1,117 +0,0 @@
|
||||
x-environment: &shopware
|
||||
image: "{{ SHOPWARE_CUSTOM_IMAGE }}"
|
||||
volumes:
|
||||
- files:/var/www/html/files
|
||||
- theme:/var/www/html/public/theme
|
||||
- media:/var/www/html/public/media
|
||||
- thumbnail:/var/www/html/public/thumbnail
|
||||
- sitemap:/var/www/html/public/sitemap
|
||||
- "{{ SHOPWARE_INIT_HOST }}:{{ SHOPWARE_INIT_DOCKER }}:ro"
|
||||
working_dir: {{ SHOPWARE_ROOT }}
|
||||
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
|
||||
# -------------------------
|
||||
# INIT (runs once per deployment)
|
||||
# -------------------------
|
||||
{% set service_name = 'init' %}
|
||||
{% set docker_restart_policy = 'no' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
{% set docker_restart_policy = DOCKER_RESTART_POLICY %}
|
||||
<<: *shopware
|
||||
container_name: "{{ SHOPWARE_INIT_CONTAINER }}"
|
||||
entrypoint: [ "sh", "{{ SHOPWARE_INIT_DOCKER }}" ]
|
||||
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{# -------------------------
|
||||
WEB (serves HTTP on 8000)
|
||||
------------------------- #}
|
||||
{% set service_name = 'web' %}
|
||||
{% set container_port = applications | get_app_conf(application_id, 'docker.services.web.port') %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
<<: *shopware
|
||||
{{ lookup('template', 'roles/docker-container/templates/build.yml.j2') | indent(4) }}
|
||||
container_name: "{{ SHOPWARE_WEB_CONTAINER }}"
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}"
|
||||
depends_on:
|
||||
init:
|
||||
condition: service_completed_successfully
|
||||
{% filter indent(4) %}
|
||||
{% include 'roles/docker-container/templates/healthcheck/http.yml.j2' %}
|
||||
{% endfilter %}
|
||||
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{# -------------------------
|
||||
WORKER (async queues)
|
||||
------------------------- #}
|
||||
{% set service_name = 'worker' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
<<: *shopware
|
||||
container_name: "{{ SHOPWARE_WORKER_CONTAINER }}"
|
||||
pull_policy: never
|
||||
entrypoint: {{ SHOPWARE_WORKER_ENTRYPOINT }}
|
||||
depends_on:
|
||||
init:
|
||||
condition: service_completed_successfully
|
||||
# @todo Activate for swarm deploy
|
||||
# deploy:
|
||||
# replicas: {{ SHOPWARE_WORKER_REPLICAS }}
|
||||
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{# -------------------------
|
||||
SCHEDULER (cron-like)
|
||||
------------------------- #}
|
||||
{% set service_name = 'scheduler' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
<<: *shopware
|
||||
container_name: "{{ SHOPWARE_SCHED_CONTAINER }}"
|
||||
pull_policy: never
|
||||
entrypoint: {{ SHOPWARE_SCHED_ENTRYPOINT }}
|
||||
depends_on:
|
||||
init:
|
||||
condition: service_completed_successfully
|
||||
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% if SHOPWARE_OPENSEARCH_ENABLED %}
|
||||
{% set service_name = 'opensearch' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
image: "{{ SHOPWARE_OPENSEARCH_IMAGE }}:{{ SHOPWARE_OPENSEARCH_VERSION }}"
|
||||
container_name: "{{ SHOPWARE_OPENSEARCH_CONTAINER }}"
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- plugins.security.disabled=true
|
||||
- bootstrap.memory_lock=true
|
||||
- OPENSEARCH_JAVA_OPTS=-Xms{{ SHOPWARE_OPENSEARCH_MEM_RESERVATION }} -Xmx{{ SHOPWARE_OPENSEARCH_MEM_RESERVATION }}
|
||||
ulimits:
|
||||
memlock: { soft: -1, hard: -1 }
|
||||
depends_on:
|
||||
init:
|
||||
condition: service_completed_successfully
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: {{ SHOPWARE_VOLUME }}
|
||||
files:
|
||||
name: {{ entity_name }}_files
|
||||
theme:
|
||||
name: {{ entity_name }}_theme
|
||||
media:
|
||||
name: {{ entity_name }}_media
|
||||
thumbnail:
|
||||
name: {{ entity_name }}_thumbnail
|
||||
sitemap:
|
||||
name: {{ entity_name }}_sitemap
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
@@ -1,33 +0,0 @@
|
||||
# DOMAIN/URL
|
||||
DOMAIN={{ domains | get_domain(application_id) }}
|
||||
APP_URL="{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
APP_DEBUG="{{ MODE_DEBUG | ternary(1, 0) }}"
|
||||
|
||||
# Shopware
|
||||
APP_ENV={{ 'dev' if (ENVIRONMENT | lower) == 'development' else 'prod' }}
|
||||
#TRUSTED_PROXIES=127.0.0.1
|
||||
INSTANCE_ID={{ application_id }}
|
||||
|
||||
# Database
|
||||
DATABASE_URL="mysql://{{ database_username }}:{{ database_password }}@{{ database_host }}:{{ database_port }}/{{ database_name }}"
|
||||
|
||||
# Redis (optional)
|
||||
{% if SHOPWARE_REDIS_ENABLED | bool %}
|
||||
REDIS_URL="redis://{{ SHOPWARE_REDIS_ADDRESS }}/0"
|
||||
CACHE_URL="redis://{{ SHOPWARE_REDIS_ADDRESS }}/1"
|
||||
MESSENGER_TRANSPORT_DSN="redis://{{ SHOPWARE_REDIS_ADDRESS }}/2"
|
||||
{% else %}
|
||||
CACHE_URL="file://cache"
|
||||
{% endif %}
|
||||
|
||||
{% if SHOPWARE_OPENSEARCH_ENABLED %}
|
||||
# Search
|
||||
ELASTICSEARCH_URL="http://opensearch:9200"
|
||||
OPENSEARCH_URL="http://opensearch:9200"
|
||||
OPENSEARCH_HOST="opensearch"
|
||||
OPENSEARCH_PORT_NUMBER="9200"
|
||||
OPENSEARCH_INITIAL_ADMIN_PASSWORD="{{ users.administrator.password }}"
|
||||
{% endif %}
|
||||
|
||||
# Mail (Mailu)
|
||||
MAILER_DSN="smtps://{{ users['no-reply'].email }}:{{ users['no-reply'].mailu_token }}@{{ SYSTEM_EMAIL.HOST }}:{{ SYSTEM_EMAIL.PORT }}"
|
||||
@@ -1,43 +0,0 @@
|
||||
# General
|
||||
application_id: "web-app-shopware"
|
||||
database_type: "mariadb"
|
||||
entity_name: "{{ application_id | get_entity_name }}"
|
||||
|
||||
# Docker
|
||||
container_port: "{{ applications | get_app_conf(application_id, 'docker.services.web.port') }}"
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
# Shopware container/image vars
|
||||
SHOPWARE_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.shopware.version') }}"
|
||||
SHOPWARE_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.shopware.image') }}"
|
||||
SHOPWARE_CUSTOM_IMAGE: "{{ SHOPWARE_IMAGE }}:{{ SHOPWARE_VERSION }}"
|
||||
SHOPWARE_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
SHOPWARE_USER: "www-data"
|
||||
SHOPWARE_ROOT: "/var/www/html"
|
||||
|
||||
# Split service container names
|
||||
SHOPWARE_INIT_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.init.name') }}"
|
||||
SHOPWARE_WEB_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.web.name') }}"
|
||||
SHOPWARE_WORKER_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.worker.name') }}"
|
||||
SHOPWARE_SCHED_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.scheduler.name') }}"
|
||||
SHOPWARE_INIT_HOST: "{{ [ docker_compose.directories.volumes, 'init.sh' ] | path_join }}"
|
||||
SHOPWARE_INIT_DOCKER: "/usr/local/bin/init.sh"
|
||||
|
||||
# Entrypoints & replicas
|
||||
SHOPWARE_WORKER_ENTRYPOINT: "{{ applications | get_app_conf(application_id, 'docker.services.worker.entrypoint') }}"
|
||||
SHOPWARE_SCHED_ENTRYPOINT: "{{ applications | get_app_conf(application_id, 'docker.services.scheduler.entrypoint') }}"
|
||||
SHOPWARE_WORKER_REPLICAS: "{{ applications | get_app_conf(application_id, 'docker.services.worker.replicas') }}"
|
||||
|
||||
# Search/Cache
|
||||
SHOPWARE_REDIS_ENABLED: "{{ applications | get_app_conf(application_id, 'docker.services.redis.enabled') }}"
|
||||
SHOPWARE_REDIS_ADDRESS: "redis:6379"
|
||||
SHOPWARE_OPENSEARCH_ENABLED: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.enabled') }}"
|
||||
SHOPWARE_OPENSEARCH_ENGINE: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.engine') }}"
|
||||
SHOPWARE_OPENSEARCH_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.image') }}"
|
||||
SHOPWARE_OPENSEARCH_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.version') }}"
|
||||
SHOPWARE_OPENSEARCH_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.name') }}"
|
||||
SHOPWARE_OPENSEARCH_MEM_RESERVATION: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.mem_reservation') }}"
|
||||
SHOPWARE_OPENSEARCH_MEM_LIMIT: "{{ applications | get_app_conf(application_id, 'docker.services.opensearch.mem_limit') }}"
|
||||
|
||||
# IAM (true if either OIDC or LDAP is enabled)
|
||||
SHOPWARE_IAM_ENABLED: "{{ applications | get_app_conf(application_id, 'features.oidc') or applications | get_app_conf(application_id, 'features.ldap') }}"
|
||||
@@ -88,15 +88,16 @@
|
||||
taiga:
|
||||
|
||||
{% set service_name = TAIGA_FRONT_SERVICE %}
|
||||
{% set container_port = 80 %}
|
||||
{{ service_name }}:
|
||||
container_name: {{ TAIGA_CONTAINER }}-{{ service_name }}
|
||||
image: "{{TAIGA_DOCKER_IMAGE_FRONTEND}}:{{ TAIGA_VERSION }}"
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
{% filter indent(4) %}
|
||||
{% include 'roles/docker-container/templates/healthcheck/http.yml.j2' %}
|
||||
{% endfilter %}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null || curl -fsS http://127.0.0.1/ >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
taiga:
|
||||
# volumes:
|
||||
@@ -151,21 +152,22 @@
|
||||
taiga:
|
||||
|
||||
{% set service_name = 'gateway' %}
|
||||
{% set container_port = 80 %}
|
||||
{{ service_name }}:
|
||||
container_name: {{ TAIGA_CONTAINER }}-{{ service_name }}
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}"
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:80"
|
||||
volumes:
|
||||
- {{ docker_repository_path }}taiga-gateway/taiga.conf:/etc/nginx/conf.d/default.conf
|
||||
- static-data:/taiga/static
|
||||
- media-data:/taiga/media
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
{% filter indent(4) %}
|
||||
{% include 'roles/docker-container/templates/healthcheck/http.yml.j2' %}
|
||||
{% endfilter %}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null || curl -fsS http://127.0.0.1/ >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
taiga:
|
||||
depends_on:
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from importlib import import_module
|
||||
|
||||
# Compute repo root (…/tests/unit/roles/web-app-desktop/lookup_plugins/docker_cards_grouped.py -> repo root)
|
||||
_THIS_DIR = os.path.dirname(__file__)
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(_THIS_DIR, "../../../../.."))
|
||||
|
||||
# Add the lookup_plugins directory to sys.path so we can import the plugin as a plain module
|
||||
_LOOKUP_DIR = os.path.join(_REPO_ROOT, "roles", "web-app-desktop", "lookup_plugins")
|
||||
if _LOOKUP_DIR not in sys.path:
|
||||
sys.path.insert(0, _LOOKUP_DIR)
|
||||
|
||||
# Import the plugin module
|
||||
plugin = import_module("docker_cards_grouped")
|
||||
LookupModule = plugin.LookupModule
|
||||
|
||||
try:
|
||||
from ansible.errors import AnsibleError
|
||||
except Exception: # Fallback for environments without full Ansible
|
||||
class AnsibleError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TestDockerCardsGroupedLookup(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.lookup = LookupModule()
|
||||
|
||||
# Menu categories with mixed-case names to verify case-insensitive sort
|
||||
self.menu_categories = {
|
||||
"B-Group": {"tags": ["b", "beta"]},
|
||||
"a-Group": {"tags": ["a", "alpha"]},
|
||||
"Zeta": {"tags": ["z"]},
|
||||
}
|
||||
|
||||
# Cards with tags; one should end up uncategorized
|
||||
self.cards = [
|
||||
{"title": "Alpha Tool", "tags": ["a"]},
|
||||
{"title": "Beta Widget", "tags": ["beta"]},
|
||||
{"title": "Zed App", "tags": ["z"]},
|
||||
{"title": "Unmatched Thing", "tags": ["x"]},
|
||||
]
|
||||
|
||||
def _run(self, cards=None, menu_categories=None):
|
||||
result = self.lookup.run(
|
||||
[cards or self.cards, menu_categories or self.menu_categories]
|
||||
)
|
||||
# Plugin returns a single-element list containing the result dict
|
||||
self.assertIsInstance(result, list)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIsInstance(result[0], dict)
|
||||
return result[0]
|
||||
|
||||
def test_categorization_and_uncategorized(self):
|
||||
data = self._run()
|
||||
self.assertIn("categorized", data)
|
||||
self.assertIn("uncategorized", data)
|
||||
|
||||
categorized = data["categorized"]
|
||||
uncategorized = data["uncategorized"]
|
||||
|
||||
# Each matching card is placed into the proper category
|
||||
self.assertIn("a-Group", categorized)
|
||||
self.assertIn("B-Group", categorized)
|
||||
self.assertIn("Zeta", categorized)
|
||||
|
||||
titles_in_a = [c["title"] for c in categorized["a-Group"]]
|
||||
titles_in_b = [c["title"] for c in categorized["B-Group"]]
|
||||
titles_in_z = [c["title"] for c in categorized["Zeta"]]
|
||||
|
||||
self.assertEqual(titles_in_a, ["Alpha Tool"])
|
||||
self.assertEqual(titles_in_b, ["Beta Widget"])
|
||||
self.assertEqual(titles_in_z, ["Zed App"])
|
||||
|
||||
# Unmatched card should be in 'uncategorized'
|
||||
self.assertEqual(len(uncategorized), 1)
|
||||
self.assertEqual(uncategorized[0]["title"], "Unmatched Thing")
|
||||
|
||||
def test_categories_sorted_alphabetically_case_insensitive(self):
|
||||
data = self._run()
|
||||
categorized = data["categorized"]
|
||||
|
||||
# Verify order is alphabetical by key, case-insensitive
|
||||
keys = list(categorized.keys())
|
||||
self.assertEqual(keys, ["a-Group", "B-Group", "Zeta"])
|
||||
|
||||
def test_multiple_tags_match_first_category_encountered(self):
|
||||
# A card that matches multiple categories should be placed
|
||||
# into the first matching category based on menu_categories iteration order.
|
||||
# Here "Dual Match" has both 'a' and 'b' tags; since "a-Group" is alphabetically
|
||||
# before "B-Group" only after sorting happens at RETURN time, we need to ensure the
|
||||
# assignment is based on menu_categories order (insertion order).
|
||||
menu_categories = {
|
||||
"B-Group": {"tags": ["b"]},
|
||||
"a-Group": {"tags": ["a"]},
|
||||
}
|
||||
cards = [{"title": "Dual Match", "tags": ["a", "b"]}]
|
||||
# The plugin iterates menu_categories in insertion order and breaks on first match,
|
||||
# so this card should end up in "B-Group".
|
||||
data = self._run(cards=cards, menu_categories=menu_categories)
|
||||
categorized = data["categorized"]
|
||||
|
||||
self.assertIn("B-Group", categorized)
|
||||
self.assertEqual([c["title"] for c in categorized["B-Group"]], ["Dual Match"])
|
||||
self.assertNotIn("a-Group", categorized) # no card added there
|
||||
|
||||
def test_missing_arguments_raises(self):
|
||||
with self.assertRaises(AnsibleError):
|
||||
self.lookup.run([]) # no args
|
||||
|
||||
with self.assertRaises(AnsibleError):
|
||||
self.lookup.run([[]]) # only one arg
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user