Commit Graph
8 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 57831d402f feat(zammad): bind Smart Assist to the platform gateway on 7.1.2
Zammad 6.5.0 has no AI surface to configure: neither `ai_provider` nor
`ai_provider_config` exists in that line, so the role could not point Smart
Assist anywhere. 7.1.2 is the newest version whose contract is still that
setting pair; from 7.2 the provider moves into an `AI::ProviderConnection`
record and the shape changes, so the bump lands here rather than on latest.
Elasticsearch is unaffected: 7.1.2 accepts >= 7.8, < 10 and the role's 8.13.4
pin stays.

The provider is written by piping files/scripts/apply/ai_provider.rb into
`rails r -` over stdin, with url, model and token passed as environment
variables instead of interpolated into Ruby source, so no quoting path can
mangle a virtual key. Order inside the script is load-bearing and documented
there: `ai_provider` is the boolean "a provider is configured" and its
validator reads `ai_provider_config`, so the config hash has to land first or
the flag is refused. `UserInfo.current_user_id` is set because `Setting.set`
needs an acting user.

This is the strongest evidence in the requirement, because it is not a
read-back of something the deploy just wrote: `Setting.set("ai_provider_config")`
runs `AI::Provider::CustomOpenAI.ping!` upstream, so the write itself is a live
round trip. An unreachable gateway or a rejected key fails the task.

Two consequences of the bump ride along, because the version does not work
without them.

`/api/v1/getting_started` answers 403 from Zammad 7.0 on, so the Playwright
helper could no longer lift a csrf token there; it uses `/api/v1/signshow`,
the unauthenticated seed. Its old fallback read `csrf_token` from a JSON body
that endpoint never returns, so the failure surfaced as a confusing throw
rather than the 403 it was.

env.j2 drops the hand-built DATABASE_URL in favour of POSTGRESQL_OPTIONS
`?pool=`. Zammad composes its DSN from the POSTGRESQL_* parts itself, so the
explicit URL both duplicated and competed with them while leaving the
connection pool at its default.

The spec decodes ZAMMAD_LITELLM_BASE_URL and ZAMMAD_LITELLM_MODEL through
decodeDotenvQuotedValue. Without it the values arrive still wrapped in the
literal quotes dotenv_quote adds and the strict comparison can never match in
compose mode -- measured as expected `"\"http://litellm:4000/v1\""` against
received `http://litellm:4000/v1`.

Verified: the spec passes against the live stack (1 passed, via
scripts/tests/e2e/rerun-spec.sh --grep=Assist, which restages the spec from the
repo), and both the contract and the Smart Assist write ran ok in a variant-2
deploy. `make quality` green on test-lint, test-unit, test-integration,
test-external.
Not covered: the no-gateway path (ZAMMAD_LITELLM_ENABLED false) has not run
since the bump, and Zammad features this deploy does not exercise -- email
channel, LDAP sync, ticket workflows -- are unproven on 7.1.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:47:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 15e7647d2f refactor(env): central TZ+LANG base included by every role env.j2
New roles/sys-svc-container/templates/env.j2 sets TZ={{ HOST_TIMEZONE }} and LANG=C.UTF-8. Every role env.j2 includes it at the top; it renders via stack_host_template, the same engine that resolves the includes in compose.yml.j2, so it works in both compose and swarm. TZ/LANG are removed from the individual role env.j2 so each value stays single-source.

New lint tests/lint/ansible/test_env_base_include.py verifies every role env.j2 includes the base and never re-sets a base-owned key (keys derived from the base file).

mattermost and gitea meta/volumes.yml drop the /etc/timezone host-path bind: swarm hard-rejects a task whose bind source is missing on the node (Debian-trixie nodes have no /etc/timezone). TZ now comes from the base env. /etc/localtime bind also dropped; the container timezone follows TZ.

Non-directive comments were removed from the touched env.j2 (banners, restatements, reference URLs, narration) so the changed files pass the comment-lint; only tool directives remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:13:57 +02:00
kevinveenbirkenbachandClaude Fable 5 30afef6bbe fix(resources): entity fallback, short compose service names + SPOT vars
resource_filter now falls back service_name -> entity_name -> hard
default, so entity-level limits keep applying to per-service compose
entries. Compose service names drop the entity prefix (kix, jitsi,
zammad, bluesky); the docker container_name keeps it via compose_only.
A new lint forbids '<entity>-'-prefixed service names.

Service names and internal ports are declared once: vars/main.yml
holds <ROLE>_<SERVICE>_SERVICE constants (container names derived via
entity_name), meta/services.yml declares ports.internal consumed
through config lookups. sso-proxy upstreams stack-qualify the origin
host in swarm because short aliases are ambiguous on shared overlays.
Stale proxy.service_key entries fixed for erpnext/shopware/nextcloud.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:05:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f4774ec145 refactor(apps): remap consumers to central engines
About 38 web-app/web-svc roles now consume the central svc-db-* and svc-dns engines via lookup('engine', ...) instead of embedded sidecars: meta/services flags, variant matrices, and env/conf hosts point at the shared services. Lets these apps run unpinned and NFS-shareable in swarm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:29:39 +02:00
kevinveenbirkenbachandClaude Opus 4.7 345d33066a refactor(web-app-zammad): drop mail-to-ticket test, auto-link OIDC by email, trim role-wide comments
- Drop test-mail-to-ticket Playwright spec + its playwright.env keys; mail/SMTP
  integration is mostly testing Mailu, not Zammad, and required either a /28
  subnet resize or an email-lookup-plugin shared-mode branch to actually pass.
- Set auth_third_party_auto_link_at_inital_login=true in apply_oidc_settings.rb
  so the first OIDC sign-in of an LDAP-synced user links to the existing record
  by email instead of returning 422. Drops the conditional LDAP user_filter
  workaround and the LDAP_OIDC_COEXISTS env wiring that propped it up.
- Replace the RUBYOPT-loaded Telegram constant shim with a bind-mounted
  TelegramHelper stub that cuts the require 'telegram/bot' chain (the gem's
  Zeitwerk bypasses Kernel#require intercepts under bootsnap).
- Strip verbose docblocks across vars/tasks/templates/scripts/playwright; keep
  only WHY-non-obvious notes (upstream gem mismatches, security defaults, the
  intentional inital-typo).
- Fix broken Docker Compose CLI docs URL in sys-svc-compose/README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:04:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3ffcc5e0ec feat(web-app-zammad): user-SPOT, LDAP source, Telegram boot shim, test fixes
User identity SPOT
- meta/users.yml gains zammad-api-bot (Basic-auth REST API bot) and
  zammad-wizard-bypass-admin (auto_wizard.json setup-bypass admin).
- vars/main.yml reads identity (login/email/firstname/lastname) via
  lookup('users', …); password stays in credentials/ to guarantee the
  shell-safe alphanumeric form needed for the Ansible→bash→docker-exec
  →Ruby ENV chain.
- auto_wizard.json.j2, apply_api_bot.rb, tasks/01_core.yml: consume the
  lookup-resolved identity.

Boot reliability
- templates/zz_telegram_constant_shim.rb.j2: pre-loaded via RUBYOPT;
  pre-declares the Telegram::Bot namespace and intercepts Kernel#require
  to refuse telegram/* loads, sidestepping the broken telegram-bot-ruby
  2.4.0 gem that crashes Zammad 6.5.0 boot. Telegram channel is inert.
- env.j2 sets RUBYOPT=-r/opt/zammad/zz_telegram_constant_shim.rb.
- tasks/01_core.yml stages the shim; compose.yml.j2 bind-mounts it into
  zammad-init/rails/websocket/scheduler/nginx.
- tasks/01_core.yml adds an explicit AutoWizard.setup post-init step
  (upstream zammad-init does NOT auto-process auto_wizard.json), which
  is what V2 (no-auth) and V3 (LDAP-only) need to skip the setup wizard.

LDAP source wire-up (TODO #1)
- files/scripts/apply_ldap_source.rb creates an LdapSource record
  pointing at svc-db-openldap (inetOrgPerson + groupOfNames mapping).
- tasks/01_core.yml runs it when services.ldap.enabled is true.

OIDC group→role mapping (TODO #3)
- templates/zz_oidc_group_role_mapping.rb.j2 patches Zammad's
  User.create_from_hash / update_from_hash to translate the RBAC
  group-path claim (e.g. /roles/web-app-zammad/administrator) into
  the matching Zammad role (Admin / Agent / Customer fallback).
- apply_oidc_settings.rb pins uid_field=preferred_username so OIDC
  matches the human-readable Keycloak username, NOT the sub UUID.
- compose.yml.j2 bind-mounts the initializer when OIDC is enabled.

Test coverage
- new test-login-via-ldap.js (driven only in LDAP-only V3).
- test-alias-domain.js accepts 2xx-direct OR 301-to-canonical
  (sys-svc-proxy renders aliases as 301 sources, not server-name
  aliases — server.yml updated to canonical=helpdesk + alias=
  zammad.helpdesk).
- test-mail-to-ticket.js: stage-level SMTP trace logging; reaches
  Mailu via host.docker.internal:465.
- test-ticket-reply-as-agent.js: scoped contenteditable lookup to
  .article-add to avoid hitting the ticket-title field.
- _shared.js: signInAsApiBot drives /api/v1/signin via the server-side
  APIRequestContext (shared cookie jar with the browser), unblocking
  the SPA-OIDC interlock.

SQL outsourcing
- templates/sql/grant_createdb.sql.j2: the CREATEDB grant now lives in
  a Jinja-templated .sql file; tasks/01_core.yml consumes it via
  lookup('template', …). Role name is rendered into the SQL by Jinja
  (NOT via database_query named_args), because psql parameter binding
  quotes string values as literals (`'zammad'`) which is invalid SQL
  for ALTER ROLE.

Misc
- meta/server.yml: canonical=helpdesk.<domain>, aliases=[zammad.helpdesk.<domain>]
  to match the 022 requirement (Decision #1).
- TODO.md drained — all seven items either landed or are explicitly
  documented as out-of-scope / brittle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:53:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 177f039b11 refactor(web-app-zammad): rename basic_auth_password → api_bot_password + SPOTs
- Rename credential basic_auth_password → api_bot_password and var
  ZAMMAD_ADMINISTRATOR_PASSWORD → ZAMMAD_API_BOT_PASSWORD to reflect
  that the value is the role-internal bot's local password, not the
  administrator's (reused for the wizard-bypass admin only because both
  need the same shell-safe alphanumeric form).
- Introduce ORGANIZATION constant (defaults to SOFTWARE_NAME) and migrate
  organization-string consumers (mattermost team, shopware shop title,
  dashboard login/logout descriptions, simpleicons "provided by",
  user-administrator description/lastname).
- Add HOST_LL_CC_TAG (BCP-47 form, e.g. en-gb) and use in auto_wizard.
- Add NETWORK_TRUSTED_PROXY_CIDRS SPOT and consume from suitecrm,
  shopware and zammad reverse-proxy configs.
- Extract ZAMMAD_WEB_PROTOCOL via lookup('tls', …, 'protocols.web')
  instead of inline 'https' if … else 'http'.
- Use database_query for CREATEDB grant so it works on shared+non-shared
  databases; back the SQL with files/sql/grant_createdb.sql SPOT.
- Read administrator firstname/lastname from users lookup in
  auto_wizard.json.j2 instead of hardcoding.
- Drop redundant elasticsearch: block from web-app-zammad services.yml
  (no consumer reads it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0f6a565dc5 feat(web-app-zammad): add Zammad helpdesk role with OIDC SSO
Adds roles/web-app-zammad/ — a brand-new web-app role deploying Zammad as
the Infinito.Nexus helpdesk with direct OpenID Connect SSO against the
shared Keycloak client. Tracks requirement 022.

Stack: ghcr.io/zammad/zammad 6.5.0 (init / railsserver / websocket /
scheduler / nginx, all driven by upstream command modes), bundled
elasticsearch 8.13.4 + bundled memcached (no central svc role exists),
central svc-db-postgres reused via lookup('database', application_id).
Variant matrix mirrors web-app-kix: V1 oidc+ldap, V2 all-false, V3 ldap-only.

Bootstrap flow (tasks/01_core.yml):
- Stage auto_wizard.json into the role's volumes dir AFTER sys-stk-full
  creates the directory; bind-mount it into zammad-init so the upstream
  rake zammad:setup:auto_wizard bypasses the setup UI on first boot.
- Stage zz_omniauth_allow_get.rb initializer + bind-mount into rails /
  websocket / scheduler / nginx, since the Zammad SPA's SSO link is a
  plain GET and omniauth 2.x forbids GET by default.
- ALTER ROLE <zammad-db-user> WITH CREATEDB so Zammad's unconditional
  `rake db:create` doesn't trip on insufficient privilege when the
  database is pre-created by svc-db-postgres.
- Apply post-bootstrap settings via apply_oidc_settings.sh
  (rails-runner inside zammad-railsserver): fqdn, http_type so Zammad
  emits the correct redirect_uri to Keycloak; auth_openid_connect +
  auth_openid_connect_credentials so the SSO button works.

Workarounds embedded in compose.yml.j2:
- zammad-nginx command override that copies /docker-entrypoint.sh to
  /tmp and patches `grep "nameserver"` to `grep -E "^nameserver"`,
  because Docker writes comment lines like `# Overrides: …` into
  /etc/resolv.conf that the upstream grep matches as substrings,
  yielding an invalid `resolver … Overrides: …` directive nginx rejects.
- DATABASE_URL pre-populated in env.j2 so post-bootstrap
  `container exec rails r` calls (which bypass the entrypoint) can
  load Rails without database.yml.

Playwright (files/playwright/playwright.spec.js, single-file lint-conform
layout): landing-reachable + administrator OIDC + biber OIDC + guest
public-landing scenarios. Personas skip-correctly via
skipUnlessServiceEnabled('oidc') under V2 / V3.

V1 end-to-end green: admin + biber Playwright pass through the full
Keycloak SSO round-trip. V2 (all-false) and V3 (ldap-only) green on
baseline reinstall with full_cycle=true.

Follow-ups indexed in roles/web-app-zammad/TODO.md: Zammad LdapSource
auto-create for V3, mail channel auto-config + end-to-end mail-to-ticket
test, Keycloak-group-to-Zammad-role mapping (Zammad doesn't ship a
built-in claim-to-role translator — currently all OIDC logins default to
Customer per Decision #5 fallback), Playwright assertion for the
`zammad.helpdesk.*` server-name alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00