refactor(gitlab): replace Omnibus with the Cloud Native GitLab composition

Deploy GitLab from the official CNG CE images as six services: webservice (puma) and sidekiq scale to the topology default, workhorse is the sole HTTP entry, gitaly and a one-shot migrations job are pinned singletons, shell runs gitlab-sshd. The Omnibus container, its Dockerfile and the GITLAB_OMNIBUS_* config groups are removed. All rails/workhorse/shell/gitaly configs and shared secrets are pre-rendered by Ansible and mounted read-only; eleven credentials land in meta/schema.yml including an rsa_pem_2048 OIDC signing key (new value_generator algorithm). Postgres, redis and S3 object storage ride the existing lookups; OIDC, SMTP, the public SSH port and the root password are preserved. compose_volumes gains a per-volume nfs:false opt-out so the gitaly repositories volume stays node-local in swarm (upstream forbids git repositories on NFS); nfs_prep skips volumes without driver_opts accordingly. The Nextcloud gitlab integration execs the CNG rails runner. ruby_single_line dies with its only consumer; ruby_dq keeps test coverage. Ports (8181/8080/2222/8075/3808/9122) and service names resolve from single SPOT declarations in meta/services.yml via vars constants; the version pin sits on the webservice image pair so the updater can bump it. Live-validated in compose up to all six services healthy and migrations completed; fresh-install-only semantics for existing Omnibus data are documented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-07-08 02:37:27 +02:00
1 parent 3ebc4113b1
commit 0bf1e8ef8c
33 files changed
+1037 -258

No files matched your search

+11 -2
View File
@@ -143,6 +143,13 @@ def compose_volumes(
optional ``name:`` field = docker volume name). Entries with
``type: config`` / ``type: secret`` emit their respective top-level
sections; everything else flows through the volumes section.
A ``type: volume`` entry may set ``nfs: false`` to opt out of the
swarm NFS rewrite: the volume then stays a plain node-local named
volume even when ``storage.backend`` is ``nfs`` (e.g. gitaly
repository storage, which upstream forbids on NFS). Any other
``nfs`` value (absent, ``true``, or the uid/gid/mode dict consumed
by the NFS subdir pre-creation) keeps the rewrite.
"""
if applications is None:
@@ -242,6 +249,8 @@ def compose_volumes(
docker_name = entry.get("name")
if docker_name:
spec["name"] = docker_name
if "nfs" in entry:
spec["nfs"] = entry["nfs"]
volumes[semantic_name] = spec
continue
@@ -282,8 +291,8 @@ def compose_volumes(
for vol_name, vol_spec in list(volumes.items()):
if not isinstance(vol_spec, dict):
continue
vol_spec.pop("nfs", None)
if swarm_nfs_enabled and not role_pinned:
nfs_opted_out = vol_spec.pop("nfs", None) is False
if swarm_nfs_enabled and not role_pinned and not nfs_opted_out:
named = vol_spec.get("name", vol_name)
vol_spec.update(_swarm_nfs_driver_opts(dir_var_lib, str(named)))
+1 -71
View File
@@ -7,79 +7,15 @@
# Output:
# "escaped_value"
#
# Safe for GitLab Omnibus (gitlab.rb), Chef, Ruby DSLs.
# Safe for Ruby DSLs (rails initializers, Chef).
class FilterModule:
def filters(self):
return {
"ruby_dq": self.ruby_double_quoted_string,
"ruby_single_line": self.ruby_single_line,
}
@staticmethod
def ruby_single_line(value):
"""Collapse a multi-line Ruby program to a single physical line.
A swarm ``docker stack deploy`` env-file value MUST be one physical
line; embedded newlines are read as separate (whitespace-named)
env entries and abort the deploy. Newlines at bracket depth 0 are
statement boundaries and become ``;``; newlines inside an open
``(``/``[``/``{`` (or a quoted string) are continuations and become
a space, which keeps Ruby hash/array literals valid.
Args:
value: Multi-line Ruby source (e.g. GITLAB_OMNIBUS_CONFIG).
Returns:
The same program as a single line.
"""
if value is None:
return ""
s = str(value)
out = []
depth = 0
in_str = False
quote = ""
i = 0
n = len(s)
while i < n:
c = s[i]
if in_str:
if c == "\\" and i + 1 < n:
out.append(c)
out.append(s[i + 1])
i += 2
continue
if c == "\n":
# A literal newline inside a string would still corrupt the
# single-line env-file; collapse it to a space.
out.append(" ")
i += 1
continue
out.append(c)
if c == quote:
in_str = False
i += 1
continue
if c in ("'", '"'):
in_str = True
quote = c
out.append(c)
elif c in "([{":
depth += 1
out.append(c)
elif c in ")]}":
depth = max(0, depth - 1)
out.append(c)
elif c == "\n":
out.append(";" if depth == 0 else " ")
else:
out.append(c)
i += 1
return "".join(out)
@staticmethod
def ruby_double_quoted_string(value):
"""
@@ -104,14 +40,8 @@ class FilterModule:
return '""'
s = str(value)
# Escape backslash first
s = s.replace("\\", "\\\\")
# Escape double quotes
s = s.replace('"', '\\"')
# Escape control characters
s = s.replace("\n", "\\n")
s = s.replace("\r", "\\r")
s = s.replace("\t", "\\t")
@@ -14,4 +14,5 @@
- (storage.backend | default('local')) == 'nfs'
- (groups['svc-storage-nfs-server'] | default([])) | length > 0
- item.value is mapping
- item.value.driver_opts is defined
- application_id not in lookup('roles_with_default_placement', 'manager')
+28 -6
View File
@@ -6,19 +6,41 @@ Accelerate your development with GitLab, an all-in-one platform for source code
## Overview
This role deploys GitLab using Docker, automating the installation, configuration, and management of your GitLab server. It integrates with an external PostgreSQL database and sets up an NGINX reverse proxy, streamlining your GitLab deployment through efficient container orchestration and customizable Ansible variables.
This role deploys GitLab from the official Cloud Native GitLab (CNG) CE images at `registry.gitlab.com/gitlab-org/build/cng/` as separate services: `webservice` (puma), `sidekiq`, `workhorse` (sole HTTP entry point), `gitaly`, `shell` (gitlab-sshd) and a one-shot `migrations` job. All images share a single version pin (`services.webservice.version`). PostgreSQL, Redis and S3-compatible object storage are wired through the platform lookups (central or sidecar), OIDC and SMTP through the platform SSO and email services. The front proxy terminates TLS and forwards HTTP to workhorse.
## Features
- **GitLab Installation:** Automatically deploys GitLab within a Docker container.
- **External PostgreSQL Database:** Configures GitLab to connect to an external PostgreSQL database.
- **NGINX Reverse Proxy Integration:** Simplifies secure access with an NGINX reverse proxy.
- **Customizable Configuration:** Easily tailor deployment settings using Ansible variables and templates.
- **CNG multi-service deployment:** webservice, sidekiq, workhorse, gitaly, gitlab-shell and a migrations one-shot from unmodified upstream images.
- **External/central PostgreSQL and Redis:** rails `database.yml`, `resque.yml`, `cable.yml` and the workhorse config are pre-rendered by Ansible and mounted read-only.
- **Consolidated object storage:** artifacts, LFS, uploads, packages, external diffs, dependency proxy, terraform state, CI secure files and pages buckets on any S3-compatible endpoint; named volumes (`gitlab_shared`, `gitlab_uploads`, `gitlab_builds`) carry the data when object storage is disabled.
- **OIDC single sign-on and SMTP:** rendered into `gitlab.yml` and an `smtp_settings.rb` initializer.
- **Git over SSH:** gitlab-sshd on the public SSH port with role-generated host keys under `<instance>/config/hostkeys/`. Back up that directory: it is not part of any named volume, and a host rebuild or instance purge regenerates the keys, so every git client then sees a host-key-changed warning until it re-trusts the new key.
## Fresh installs only
The role provisions new GitLab instances. Volumes, secrets and backups of a pre-CNG Omnibus deployment (`gitlab_config`, `gitlab_data`, `/etc/gitlab/gitlab-secrets.json`) are not migrated or restorable into the CNG layout; deploy against a fresh database and empty volumes.
## Gitaly data locality
The `gitlab_repositories` volume is declared `nfs: false`, so in swarm mode it stays a plain node-local volume instead of being rewritten to the shared NFS backend. Gitaly is pinned to a single replica. When a swarm reschedule moves the gitaly task to another node, the repositories stay on the previous node's local volume; move the volume data manually before rescheduling gitaly.
## Upgrades
On each version bump of `services.webservice.version`:
1. Follow the upstream upgrade path stops between the old and new version; the `migrations` one-shot fails hard on skipped stops.
2. Diff the CNG repo `dev/` config templates (`webservice-config`, `sidekiq-config`, `workhorse-config`, `shell-config`, `gitaly-config`) between the two tags and mirror schema changes into `templates/config/`.
## Omissions
- `kas` (Kubernetes agent server, workspaces), `pages`, `registry` and `mailroom` are not deployed; `gitlab.yml` disables them.
- Images are the CE edition (`gitlab-webservice-ce`, `gitlab-sidekiq-ce`, `gitlab-workhorse-ce`, `gitlab-rails-ce`); switch the image keys in `meta/services.yml` to the `-ee` variants for an EE deployment.
## Further Resources
- [GitLab Official Website](https://about.gitlab.com/)
- [Running GitLab on Docker](https://ralph.blog.imixs.com/2019/06/09/running-gitlab-on-docker/)
- [Cloud Native GitLab (CNG) images](https://gitlab.com/gitlab-org/build/CNG)
- [GitLab Helm charts documentation](https://docs.gitlab.com/charts/)
## Credits
-6
View File
@@ -1,6 +0,0 @@
ARG GITLAB_IMAGE
ARG GITLAB_VERSION
FROM ${GITLAB_IMAGE}:${GITLAB_VERSION}
RUN sed -i '1i require_relative "../redis"' \
/opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/instrumentation/redis.rb
+46
View File
@@ -0,0 +1,46 @@
---
credentials:
secret_key_base:
description: Rails secret key base used to verify signed cookies and tokens
algorithm: random_hex
validation: "^[a-f0-9]{128}$"
otp_key_base:
description: Key base encrypting two-factor authentication secrets
algorithm: random_hex
validation: "^[a-f0-9]{128}$"
db_key_base:
description: Key base encrypting database columns (CI variables, tokens)
algorithm: random_hex
validation: "^[a-f0-9]{128}$"
encrypted_settings_key_base:
description: Key base encrypting settings files
algorithm: random_hex
validation: "^[a-f0-9]{128}$"
active_record_encryption_primary_key:
description: Primary key for Active Record encrypted columns
algorithm: random_hex_16
validation: "^[a-f0-9]{32}$"
active_record_encryption_deterministic_key:
description: Deterministic key for Active Record encrypted columns
algorithm: random_hex_16
validation: "^[a-f0-9]{32}$"
active_record_encryption_key_derivation_salt:
description: Key derivation salt for Active Record encryption
algorithm: random_hex_16
validation: "^[a-f0-9]{32}$"
gitlab_shell_secret:
description: Shared token authenticating gitlab-shell and gitaly against the rails API
algorithm: alphanumeric
validation: "^[A-Za-z0-9]{64}$"
gitaly_token:
description: Auth token shared between rails and the gitaly gRPC listener
algorithm: alphanumeric
validation: "^[A-Za-z0-9]{64}$"
workhorse_secret:
description: 32-byte value shared between workhorse and rails, mounted base64-encoded
algorithm: random_hex_16
validation: "^[a-f0-9]{32}$"
openid_connect_signing_key:
description: RSA private key signing GitLab's own OpenID Connect provider tokens
algorithm: rsa_pem_2048
validation: "^-----BEGIN RSA PRIVATE KEY-----[\\s\\S]+-----END RSA PRIVATE KEY-----\\s*$"
+67 -10
View File
@@ -41,29 +41,86 @@ gitlab:
bond: 1
enabled: true
shared: true
image: gitlab/gitlab-ee
version: 19.1.1-ee.0
name: gitlab
min_storage: 30GB
cpus: "4.0"
mem_reservation: 8g
mem_limit: 12g
pids_limit: 4096
# Exception: the entity runs no container of its own (webservice/sidekiq/workhorse/gitaly/shell/migrations carry the real budgets); these keys only satisfy the entity resource contract
cpus: "0.1"
mem_reservation: 128m
mem_limit: 256m
pids_limit: 128
ports:
local:
http: 8024
public:
ssh: 2202
internal:
http: 80
ssh: 22
http: 8181
ssh: 2222
proxy:
service_key: gitlab
service_key: workhorse
run_after:
- web-app-matomo
- web-app-keycloak
- web-app-mailu
lifecycle: beta
webservice:
image: registry.gitlab.com/gitlab-org/build/cng/gitlab-webservice-ce
version: v19.1.1
name: gitlab-webservice
ports:
internal:
http: 8080
cpus: "2.0"
mem_reservation: 4g
mem_limit: 6g
pids_limit: 2048
sidekiq:
image: registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce
name: gitlab-sidekiq
cpus: "1.0"
mem_reservation: 2g
mem_limit: 3g
pids_limit: 1024
ports:
internal:
http: 3808
workhorse:
image: registry.gitlab.com/gitlab-org/build/cng/gitlab-workhorse-ce
name: gitlab-workhorse
cpus: "0.5"
mem_reservation: 256m
mem_limit: 512m
pids_limit: 256
gitaly:
# nocheck: single-replica
replicas: 1
image: registry.gitlab.com/gitlab-org/build/cng/gitaly
name: gitlab-gitaly
ports:
internal:
grpc: 8075
cpus: "1.0"
mem_reservation: 1g
mem_limit: 2g
pids_limit: 1024
shell:
image: registry.gitlab.com/gitlab-org/build/cng/gitlab-shell
name: gitlab-shell
ports:
internal:
metrics: 9122
cpus: "0.25"
mem_reservation: 128m
mem_limit: 256m
pids_limit: 256
migrations:
# nocheck: single-replica
replicas: 1
image: registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce
name: gitlab-migrations
cpus: "1.0"
mem_reservation: 1g
mem_limit: 2g
pids_limit: 512
seaweedfs:
bond: 1
enabled: "{{ 'web-app-seaweedfs' in group_names }}"
+107 -12
View File
@@ -1,19 +1,114 @@
---
config:
# Exception: nfs false keeps gitaly repository storage node-local in swarm; upstream forbids git repositories on NFS
repositories:
type: volume
name: gitlab_config
name: gitlab_repositories
nfs: false
mounts:
- service: gitlab
target: /etc/gitlab
logs:
- service: gitaly
target: /home/git/repositories
shared:
type: volume
name: gitlab_logs
name: gitlab_shared
mounts:
- service: gitlab
target: /var/log/gitlab
data:
- service: webservice
target: /srv/gitlab/shared
- service: sidekiq
target: /srv/gitlab/shared
- service: workhorse
target: /srv/gitlab/shared
uploads:
type: volume
name: gitlab_data
name: gitlab_uploads
mounts:
- service: gitlab
target: /var/opt/gitlab
- service: webservice
target: /srv/gitlab/public/uploads
- service: sidekiq
target: /srv/gitlab/public/uploads
- service: workhorse
target: /srv/gitlab/public/uploads
builds:
type: volume
name: gitlab_builds
mounts:
- service: webservice
target: /srv/gitlab/builds
- service: sidekiq
target: /srv/gitlab/builds
rails_templates:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'rails'] | path_join }}"
read_only: true
mounts:
- service: webservice
target: /var/opt/gitlab/config/templates
- service: sidekiq
target: /var/opt/gitlab/config/templates
- service: migrations
target: /var/opt/gitlab/config/templates
rails_secrets:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'secrets'] | path_join }}"
read_only: true
mounts:
- service: webservice
target: /var/opt/gitlab/config/secrets
- service: sidekiq
target: /var/opt/gitlab/config/secrets
- service: migrations
target: /var/opt/gitlab/config/secrets
workhorse_templates:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'workhorse'] | path_join }}"
read_only: true
mounts:
- service: workhorse
target: /var/opt/gitlab/config/templates
workhorse_secret:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'secrets', '.gitlab_workhorse_secret'] | path_join }}"
read_only: true
mounts:
- service: workhorse
target: /etc/gitlab/gitlab-workhorse/secret
- service: webservice
target: /srv/gitlab/.gitlab_workhorse_secret
shell_templates:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'shell'] | path_join }}"
read_only: true
mounts:
- service: shell
target: /srv/gitlab-config
shell_secret:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'secrets', '.gitlab_shell_secret'] | path_join }}"
read_only: true
mounts:
- service: shell
target: /srv/gitlab-secrets/.gitlab_shell_secret
gitaly_config:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'gitaly'] | path_join }}"
read_only: true
mounts:
- service: gitaly
target: /etc/gitaly
hostkeys:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'hostkeys'] | path_join }}"
read_only: true
mounts:
- service: shell
target: /srv/ssh-hostkeys
smtp_initializer:
type: bind
source: "{{ [lookup('container', 'web-app-gitlab', 'directories.config'), 'smtp_settings.rb'] | path_join }}"
read_only: true
mounts:
- service: webservice
target: /srv/gitlab/config/initializers/smtp_settings.rb
when: "{{ lookup('email', 'web-app-gitlab').enabled | bool }}"
- service: sidekiq
target: /srv/gitlab/config/initializers/smtp_settings.rb
when: "{{ lookup('email', 'web-app-gitlab').enabled | bool }}"
+20 -2
View File
@@ -1,6 +1,24 @@
---
- include_tasks: utils/once/flag.yml
- name: "🧩 load docker, db and proxy for {{ application_id }}"
- name: "📂 Include setup for domain '{{ lookup('domain', application_id) }}'"
include_role:
name: sys-stk-full
name: sys-stk-front-proxy
vars:
domain: "{{ lookup('domain', application_id) }}"
http_port: "{{ lookup('config', application_id, 'services.gitlab.ports.local.http') }}"
- name: "🧩 load docker and db for {{ application_id }}"
include_role:
name: sys-stk-backend
vars:
compose_handlers_flush: false
- name: "📄 Render GitLab CNG configuration"
include_tasks: 02_config.yml
- name: 🚿 Flush handlers to start the GitLab stack
meta: flush_handlers
- name: "⏳ Wait for GitLab services"
include_tasks: 03_wait.yml
+131
View File
@@ -0,0 +1,131 @@
---
- name: "📁 Ensure GitLab config directories for '{{ application_id }}'"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ GITLAB_CONFIG_RAILS_DIR }}"
- "{{ GITLAB_CONFIG_SECRETS_DIR }}"
- "{{ GITLAB_CONFIG_WORKHORSE_DIR }}"
- "{{ GITLAB_CONFIG_SHELL_DIR }}"
- "{{ GITLAB_CONFIG_GITALY_DIR }}"
- "{{ GITLAB_CONFIG_HOSTKEYS_DIR }}"
- name: "📄 Render GitLab CNG config files (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml') }}"
vars:
role_templates:
- src: "config/gitlab.yml.j2"
dest: "{{ [GITLAB_CONFIG_RAILS_DIR, 'gitlab.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
- src: "config/database.yml.j2"
dest: "{{ [GITLAB_CONFIG_RAILS_DIR, 'database.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
- src: "config/resque.yml.j2"
dest: "{{ [GITLAB_CONFIG_RAILS_DIR, 'resque.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
- src: "config/cable.yml.j2"
dest: "{{ [GITLAB_CONFIG_RAILS_DIR, 'cable.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
- src: "config/secrets.yml.j2"
dest: "{{ [GITLAB_CONFIG_RAILS_DIR, 'secrets.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0400"
- src: "config/sidekiq_queues.yml.j2"
dest: "{{ [GITLAB_CONFIG_RAILS_DIR, 'sidekiq_queues.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0444"
- src: "config/smtp_settings.rb.j2"
dest: "{{ [GITLAB_CONFIG_DIR, 'smtp_settings.rb'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
condition: "{{ GITLAB_EMAIL_ENABLED | bool }}"
- src: "config/gitlab_shell_secret.j2"
dest: "{{ [GITLAB_CONFIG_SECRETS_DIR, '.gitlab_shell_secret'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0400"
- src: "config/gitlab_shell_secret.j2"
dest: "{{ [GITLAB_CONFIG_DIR, 'gitaly', '.gitlab_shell_secret'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0400"
- src: "config/workhorse_secret.j2"
dest: "{{ [GITLAB_CONFIG_SECRETS_DIR, '.gitlab_workhorse_secret'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0400"
- src: "config/workhorse-config.toml.j2"
dest: "{{ [GITLAB_CONFIG_WORKHORSE_DIR, 'workhorse-config.toml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
- src: "config/shell-config.yml.j2"
dest: "{{ [GITLAB_CONFIG_SHELL_DIR, 'config.yml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0444"
- src: "config/gitaly-config.toml.j2"
dest: "{{ [GITLAB_CONFIG_GITALY_DIR, 'config.toml'] | path_join }}"
owner: "1000"
group: "1000"
mode: "0440"
- name: "🔑 Generate GitLab SSH host keys (on stack host)"
community.crypto.openssh_keypair:
path: "{{ [GITLAB_CONFIG_HOSTKEYS_DIR, item.name] | path_join }}"
type: "{{ item.type }}"
size: "{{ item.size | default(omit) }}"
owner: "1000"
group: "1000"
mode: "0400"
loop:
- { name: ssh_host_rsa_key, type: rsa, size: 4096 }
- { name: ssh_host_ed25519_key, type: ed25519 }
loop_control:
label: "{{ item.name }}"
delegate_to: "{{ STACK_HOST }}"
run_once: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
notify: compose up
- name: "[swarm] Slurp GitLab SSH host keys from stack host"
ansible.builtin.slurp:
src: "{{ [GITLAB_CONFIG_HOSTKEYS_DIR, item] | path_join }}"
loop:
- ssh_host_rsa_key
- ssh_host_ed25519_key
delegate_to: "{{ STACK_HOST }}"
run_once: true
register: gitlab_hostkeys_slurp
when: DEPLOYMENT_MODE == 'swarm'
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "[swarm] Distribute GitLab SSH host keys to swarm peers"
ansible.builtin.copy:
content: "{{ item.content | b64decode }}"
dest: "{{ [GITLAB_CONFIG_HOSTKEYS_DIR, item.item] | path_join }}"
owner: "1000"
group: "1000"
mode: "0400"
loop: "{{ gitlab_hostkeys_slurp.results }}"
loop_control:
label: "{{ item.item }}"
when:
- DEPLOYMENT_MODE == 'swarm'
- not (IS_STACK_HOST | bool)
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
notify: compose up
+9
View File
@@ -0,0 +1,9 @@
---
- name: "🔎 Wait until '{{ item }}' container is healthy"
ansible.builtin.include_tasks: 03a_wait_one.yml
loop:
- "{{ GITLAB_WEBSERVICE_SERVICE_NAME }}"
- "{{ GITLAB_WORKHORSE_SERVICE_NAME }}"
- "{{ GITLAB_SIDEKIQ_SERVICE_NAME }}"
- "{{ GITLAB_GITALY_SERVICE_NAME }}"
- "{{ GITLAB_SHELL_SERVICE_NAME }}"
@@ -0,0 +1,18 @@
---
- name: "🔍 Resolve '{{ item }}' hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ item }}"
resolve_output_prefix: GITLAB_WAIT
resolve_compose_cid: "{{ lookup('container_address', application_id, item) }}"
- name: "🔎 Check health status of '{{ item }}' container"
ansible.builtin.shell: >
container inspect --format '{{ "{{.State.Health.Status}}" }}'
{{ GITLAB_WAIT_LOCAL_CID | quote }}
register: healthcheck
retries: 90
delay: 10
until: healthcheck.rc == 0 and (healthcheck.stdout | trim) == "healthy"
changed_when: false
delegate_to: "{{ GITLAB_WAIT_HOST_NODE }}"
+110 -16
View File
@@ -1,23 +1,117 @@
{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}
{% set service_name = 'gitlab' %}
{% set service_name = GITLAB_WEBSERVICE_SERVICE_NAME %}
{{ service_name }}:
{{ lookup('container_image', application_id, 'gitlab', custom=True) }}
hostname: '{{ GITLAB_HOSTNAME }}'
{{ lookup('compose_only', 'container_name', GITLAB_CONTAINER) }}
{{ lookup('compose_only', 'pull_policy', 'never') }}
build:
context: .
dockerfile: Dockerfile
args:
GITLAB_IMAGE: "{{ GITLAB_IMAGE }}"
GITLAB_VERSION: "{{ GITLAB_VERSION }}"
{{ lookup('compose_only', 'container_name', GITLAB_WEBSERVICE_CONTAINER) }}
{{ lookup('container_image', application_id, service_name, version=GITLAB_VERSION) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_ports',
['gitlab', 'http', DOCKER_BIND_HOST],
['gitlab', 'ssh']) | indent(4) }}
{{ lookup('container_volumes', application_id, 'gitlab') | indent(4) }}
shm_size: '256m'
environment:
CONFIG_TEMPLATE_DIRECTORY: "/var/opt/gitlab/config/templates"
CONFIG_DIRECTORY: "/srv/gitlab/config"
GITLAB_WEBSERVER: "PUMA"
ACTION_CABLE_IN_APP: "true"
command: ["/scripts/wait-for-deps", "/scripts/process-wrapper"]
healthcheck:
test: ["CMD", "/scripts/healthcheck"]
interval: 30s
timeout: 10s
retries: 5
start_period: 600s
{{ lookup('container_depends_on', application_id, extra={
GITLAB_MIGRATIONS_SERVICE_NAME: {'condition': 'service_completed_successfully'},
}) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% set service_name = GITLAB_WORKHORSE_SERVICE_NAME %}
{{ service_name }}:
{{ lookup('compose_only', 'container_name', GITLAB_WORKHORSE_CONTAINER) }}
{{ lookup('container_image', application_id, service_name, version=GITLAB_VERSION) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
environment:
CONFIG_TEMPLATE_DIRECTORY: "/var/opt/gitlab/config/templates"
CONFIG_DIRECTORY: "/srv/gitlab/config"
GITLAB_WORKHORSE_AUTH_BACKEND: "http://{{ GITLAB_WEBSERVICE_SERVICE_NAME }}:{{ GITLAB_WEBSERVICE_PORT }}"
GITLAB_WORKHORSE_EXTRA_ARGS: "-cableBackend http://{{ GITLAB_WEBSERVICE_SERVICE_NAME }}:{{ GITLAB_WEBSERVICE_PORT }}"
command: ["/scripts/start-workhorse"]
healthcheck:
test: ["CMD", "/scripts/healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s
{{ lookup('container_ports', ['gitlab', 'http', DOCKER_BIND_HOST]) | indent(4) }}
{{ lookup('container_depends_on', application_id, extra={
GITLAB_WEBSERVICE_SERVICE_NAME: {'condition': 'service_started'},
}) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% set service_name = GITLAB_SIDEKIQ_SERVICE_NAME %}
{% set container_healthcheck = 'liveness' %}
{% set container_healthcheck_start_period = '300s' %}
{{ service_name }}:
{{ lookup('compose_only', 'container_name', GITLAB_SIDEKIQ_CONTAINER) }}
{{ lookup('container_image', application_id, service_name, version=GITLAB_VERSION) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
environment:
CONFIG_TEMPLATE_DIRECTORY: "/var/opt/gitlab/config/templates"
CONFIG_DIRECTORY: "/srv/gitlab/config"
command: ["/scripts/wait-for-deps", "/scripts/process-wrapper"]
{% include 'roles/sys-svc-container/templates/healthcheck/tcp.yml.j2' %}
{{ lookup('container_depends_on', application_id, extra={
GITLAB_MIGRATIONS_SERVICE_NAME: {'condition': 'service_completed_successfully'},
}) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% set service_name = GITLAB_GITALY_SERVICE_NAME %}
{{ service_name }}:
{{ lookup('compose_only', 'container_name', GITLAB_GITALY_CONTAINER) }}
{{ lookup('container_image', application_id, service_name, version=GITLAB_VERSION) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
healthcheck:
test: ["CMD", "/scripts/healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% set service_name = GITLAB_SHELL_SERVICE_NAME %}
{{ service_name }}:
{{ lookup('compose_only', 'container_name', GITLAB_SHELL_CONTAINER) }}
{{ lookup('container_image', application_id, service_name, version=GITLAB_VERSION) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
environment:
CONFIG_TEMPLATE_DIRECTORY: "/srv/gitlab-config"
CONFIG_DIRECTORY: "/srv/gitlab-shell"
SSH_DAEMON: "gitlab-sshd"
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/{{ GITLAB_SSHD_PORT }}"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
{{ lookup('container_ports', ['gitlab', 'ssh']) | indent(4) }}
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% set service_name = GITLAB_MIGRATIONS_SERVICE_NAME %}
{% set docker_restart_policy = 'on-failure' %}
{{ service_name }}:
{{ lookup('compose_only', 'container_name', GITLAB_MIGRATIONS_CONTAINER) }}
{{ lookup('container_image', application_id, service_name, version=GITLAB_VERSION) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
environment:
CONFIG_TEMPLATE_DIRECTORY: "/var/opt/gitlab/config/templates"
CONFIG_DIRECTORY: "/srv/gitlab/config"
BYPASS_SCHEMA_VERSION: "true"
command: ["/scripts/wait-for-deps", "/scripts/db-migrate"]
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{{ lookup('compose_volumes', application_id) }}
@@ -0,0 +1,4 @@
---
production:
adapter: redis
url: {{ GITLAB_REDIS_URL | to_json }}
@@ -0,0 +1,21 @@
---
production:
main:
adapter: postgresql
encoding: unicode
database: {{ lookup('database', application_id, 'name') | to_json }}
username: {{ lookup('database', application_id, 'username') | to_json }}
password: {{ lookup('database', application_id, 'password') | to_json }}
host: {{ lookup('database', application_id, 'host') | to_json }}
port: {{ lookup('database', application_id, 'port') }}
pool: {{ POSTGRES_ALLOWED_AVG_CONNECTIONS }}
ci:
adapter: postgresql
encoding: unicode
database: {{ lookup('database', application_id, 'name') | to_json }}
username: {{ lookup('database', application_id, 'username') | to_json }}
password: {{ lookup('database', application_id, 'password') | to_json }}
host: {{ lookup('database', application_id, 'host') | to_json }}
port: {{ lookup('database', application_id, 'port') }}
pool: {{ POSTGRES_ALLOWED_AVG_CONNECTIONS }}
database_tasks: false
@@ -0,0 +1,21 @@
bin_dir = "/usr/local/bin"
runtime_dir = "/home/git"
listen_addr = "0.0.0.0:{{ GITLAB_GITALY_PORT }}"
[[storage]]
name = "default"
path = "/home/git/repositories"
[auth]
token = {{ GITLAB_GITALY_TOKEN | to_json }}
[logging]
format = "json"
level = "info"
[gitlab]
secret_file = "/etc/gitaly/.gitlab_shell_secret"
url = "http://{{ GITLAB_WEBSERVICE_SERVICE_NAME }}:{{ GITLAB_WEBSERVICE_PORT }}/"
[hooks]
custom_hooks_dir = "/home/git/custom_hooks"
@@ -0,0 +1,117 @@
---
production:
gitlab:
host: {{ GITLAB_HOSTNAME | to_json }}
port: {{ GITLAB_WEB_PORT }}
https: {{ GITLAB_TLS_ENABLED | bool | string | lower }}
email_enabled: {{ GITLAB_EMAIL_ENABLED | bool | string | lower }}
{% if GITLAB_EMAIL_ENABLED %}
email_from: {{ GITLAB_EMAIL['from'] | to_json }}
email_display_name: "GitLab"
email_reply_to: {{ GITLAB_EMAIL['from'] | to_json }}
{% endif %}
{% if GITLAB_OBJSTORE_ENABLED %}
object_store:
enabled: true
proxy_download: true
connection:
provider: "AWS"
aws_access_key_id: {{ GITLAB_OBJSTORE_ACCESS_KEY | to_json }}
aws_secret_access_key: {{ GITLAB_OBJSTORE_SECRET_KEY | to_json }}
region: {{ GITLAB_OBJSTORE_REGION | to_json }}
endpoint: {{ GITLAB_OBJSTORE_ENDPOINT | to_json }}
path_style: true
objects:
artifacts:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/artifacts') | to_json }}
external_diffs:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/external-diffs') | to_json }}
lfs:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/lfs') | to_json }}
uploads:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/uploads') | to_json }}
packages:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/packages') | to_json }}
dependency_proxy:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/dependency-proxy') | to_json }}
terraform_state:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/terraform-state') | to_json }}
ci_secure_files:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/ci-secure-files') | to_json }}
pages:
bucket: {{ (GITLAB_OBJSTORE_BUCKET ~ '/pages') | to_json }}
{% else %}
object_store:
enabled: false
{% endif %}
incoming_email:
enabled: false
artifacts:
enabled: true
lfs:
enabled: true
pages:
enabled: false
mattermost:
enabled: false
registry:
enabled: false
ldap:
enabled: false
kerberos:
enabled: false
{% if GITLAB_OIDC_ENABLED %}
omniauth:
enabled: true
allow_single_sign_on: ["openid_connect"]
block_auto_created_users: false
auto_link_user: ["openid_connect"]
providers:
- name: "openid_connect"
label: {{ GITLAB_OIDC_LABEL | to_json }}
args:
name: "openid_connect"
scope:
- "openid"
- "profile"
- "email"
response_type: "code"
issuer: {{ GITLAB_OIDC_ISSUER | to_json }}
discovery: true
uid_field: {{ GITLAB_OIDC_UID_FIELD | to_json }}
pkce: true
client_options:
identifier: {{ GITLAB_OIDC_CLIENT_ID | to_json }}
secret: {{ GITLAB_OIDC_CLIENT_SECRET | to_json }}
redirect_uri: {{ GITLAB_OIDC_REDIRECT_URI | to_json }}
{% else %}
omniauth:
enabled: false
{% endif %}
gitaly:
token: {{ GITLAB_GITALY_TOKEN | to_json }}
repositories:
storages:
default:
path: /var/opt/gitlab/repo
gitaly_address: "tcp://{{ GITLAB_GITALY_SERVICE_NAME }}:{{ GITLAB_GITALY_PORT }}"
backup:
path: "tmp/backups"
gitlab_shell:
path: /home/git/gitlab-shell/
hooks_path: /home/git/gitlab-shell/hooks/
secret_file: /var/opt/gitlab/config/secrets/.gitlab_shell_secret
upload_pack: true
receive_pack: true
ssh_host: {{ GITLAB_HOSTNAME | to_json }}
ssh_port: {{ GITLAB_SSH_PORT }}
git:
bin_path: /usr/bin/git
monitoring:
ip_whitelist:
- 127.0.0.0/8
- ::1/128
sidekiq_health_checks:
enabled: true
address: 0.0.0.0
port: {{ GITLAB_SIDEKIQ_HEALTH_PORT }}
@@ -0,0 +1 @@
{{ GITLAB_SHELL_SECRET }}
@@ -0,0 +1,3 @@
---
production:
url: {{ GITLAB_REDIS_URL | to_json }}
@@ -0,0 +1,13 @@
---
production:
secret_key_base: {{ lookup('config', application_id, 'credentials.secret_key_base') | to_json }}
otp_key_base: {{ lookup('config', application_id, 'credentials.otp_key_base') | to_json }}
db_key_base: {{ lookup('config', application_id, 'credentials.db_key_base') | to_json }}
encrypted_settings_key_base: {{ lookup('config', application_id, 'credentials.encrypted_settings_key_base') | to_json }}
openid_connect_signing_key: |
{{ lookup('config', application_id, 'credentials.openid_connect_signing_key') | trim | indent(4, true) }}
active_record_encryption_primary_key:
- {{ lookup('config', application_id, 'credentials.active_record_encryption_primary_key') | to_json }}
active_record_encryption_deterministic_key:
- {{ lookup('config', application_id, 'credentials.active_record_encryption_deterministic_key') | to_json }}
active_record_encryption_key_derivation_salt: {{ lookup('config', application_id, 'credentials.active_record_encryption_key_derivation_salt') | to_json }}
@@ -0,0 +1,15 @@
---
user: git
gitlab_url: "http://{{ GITLAB_WORKHORSE_SERVICE_NAME }}:{{ GITLAB_WORKHORSE_PORT }}/"
secret_file: /srv/gitlab-secrets/.gitlab_shell_secret
auth_file: "/home/git/.ssh/authorized_keys"
log_file: "/dev/stdout"
log_level: INFO
audit_usernames: false
sshd:
listen: "[::]:{{ GITLAB_SSHD_PORT }}"
web_listen: "0.0.0.0:{{ GITLAB_SHELL_METRICS_PORT }}"
concurrent_sessions_limit: 100
host_key_files:
- /srv/ssh-hostkeys/ssh_host_rsa_key
- /srv/ssh-hostkeys/ssh_host_ed25519_key
@@ -0,0 +1,61 @@
---
:queues:
- [post_receive, 5]
- [merge, 5]
- [update_merge_requests, 3]
- [process_commit, 3]
- [new_note, 2]
- [new_issue, 2]
- [new_merge_request, 2]
- [build, 2]
- [pipeline, 2]
- [pipeline_processing, 5]
- [pipeline_default, 3]
- [pipeline_cache, 3]
- [pipeline_hooks, 2]
- [gitlab_shell, 2]
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
- [upload_checksum, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
- [delete_user, 1]
- [delete_merged_branches, 1]
- [authorized_projects, 1]
- [expire_build_instance_artifacts, 1]
- [group_destroy, 1]
- [irker, 1]
- [namespaceless_project_destroy, 1]
- [project_cache, 1]
- [project_destroy, 1]
- [project_export, 1]
- [web_hook, 1]
- [repository_check, 1]
- [git_garbage_collect, 1]
- [reactive_caching, 1]
- [cronjob, 1]
- [default, 1]
- [pages, 1]
- [system_hook_push, 1]
- [update_user_activity, 1]
- [propagate_service_template, 1]
- [background_migration, 1]
- [gcp_cluster, 1]
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [ldap_group_sync, 2]
- [geo, 1]
- [repository_update_mirror, 1]
- [repository_update_remote_mirror, 1]
- [project_update_repository_storage, 1]
- [admin_emails, 1]
- [geo_repository_update, 1]
- [elastic_batch_project_indexer, 1]
- [elastic_indexer, 1]
- [elastic_commit_indexer, 1]
- [export_csv, 1]
- [object_storage_upload, 1]
@@ -0,0 +1,12 @@
Rails.application.config.action_mailer.delivery_method = :smtp
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
address: {{ GITLAB_EMAIL.host | ruby_dq }},
port: {{ GITLAB_EMAIL.port }},
user_name: {{ GITLAB_EMAIL.username | ruby_dq }},
password: {{ GITLAB_EMAIL.password | ruby_dq }},
domain: {{ GITLAB_EMAIL.domain | ruby_dq }},
authentication: {{ ':login' if (GITLAB_EMAIL.auth | bool) else 'nil' }},
enable_starttls_auto: {{ GITLAB_EMAIL.start_tls | bool | string | lower }},
tls: {{ GITLAB_EMAIL.tls | bool | string | lower }}
}
@@ -0,0 +1,15 @@
shutdown_timeout = "10s"
[redis]
URL = "tcp://{{ GITLAB_REDIS_HOST }}:{{ GITLAB_REDIS_PORT }}"
{% if GITLAB_REDIS_PASSWORD %}
Password = {{ GITLAB_REDIS_PASSWORD | to_json }}
{% endif %}
[image_resizer]
max_scaler_procs = 100
max_filesize = 250000
[[listeners]]
network = "tcp"
addr = "0.0.0.0:{{ GITLAB_WORKHORSE_PORT }}"
@@ -0,0 +1 @@
{{ GITLAB_WORKHORSE_SECRET | b64encode }}
+7 -5
View File
@@ -1,5 +1,7 @@
{% if DEPLOYMENT_MODE == 'swarm' %}
GITLAB_OMNIBUS_CONFIG={{ (GITLAB_OMNIBUS_ALL | join('\n')) | ruby_single_line | dotenv_quote }}
{% else %}
GITLAB_OMNIBUS_CONFIG={{ (GITLAB_OMNIBUS_ALL | join('\n')) | dotenv_quote }}
{% endif %}
GITLAB_HOST={{ GITLAB_HOSTNAME }}
GITLAB_PORT={{ GITLAB_WEB_PORT }}
GITLAB_SSH_PORT={{ GITLAB_SSH_PORT }}
WAIT_FOR_TIMEOUT=1800
SIDEKIQ_TIMEOUT=25
ENABLE_BOOTSNAP=true
GITLAB_ROOT_PASSWORD={{ GITLAB_INIT_ROOT_PASSWORD | dotenv_quote }}
+44 -86
View File
@@ -2,8 +2,21 @@
application_id: "web-app-gitlab"
entity_name: "{{ application_id | get_entity_name }}"
swarm_stack_converge_retries: 600
GITLAB_URL: "{{ lookup('tls', application_id, 'url.base') }}"
GITLAB_HOSTNAME: "{{ lookup('domain',application_id) }}"
GITLAB_HOSTNAME: "{{ lookup('domain', application_id) }}"
GITLAB_TLS_ENABLED: "{{ lookup('tls', application_id, 'enabled') | bool }}"
GITLAB_WEB_PORT: "{{ lookup('tls', application_id, 'ports.web') }}"
GITLAB_VERSION: "{{ lookup('config', application_id, 'services.webservice.version') }}"
GITLAB_SSH_PORT: "{{ lookup('config', application_id, 'services.gitlab.ports.public.ssh') }}"
GITLAB_WORKHORSE_PORT: "{{ lookup('config', application_id, 'services.gitlab.ports.internal.http') }}"
GITLAB_SSHD_PORT: "{{ lookup('config', application_id, 'services.gitlab.ports.internal.ssh') }}"
GITLAB_WEBSERVICE_PORT: "{{ lookup('config', application_id, 'services.webservice.ports.internal.http') }}"
GITLAB_GITALY_PORT: "{{ lookup('config', application_id, 'services.gitaly.ports.internal.grpc') }}"
GITLAB_SIDEKIQ_HEALTH_PORT: "{{ lookup('config', application_id, 'services.sidekiq.ports.internal.http') }}"
GITLAB_SHELL_METRICS_PORT: "{{ lookup('config', application_id, 'services.shell.ports.internal.metrics') }}"
GITLAB_EMAIL: "{{ lookup('email', application_id) }}"
GITLAB_EMAIL_ENABLED: "{{ GITLAB_EMAIL.enabled | bool }}"
@@ -14,70 +27,9 @@ GITLAB_OIDC_UID_FIELD: "{{ OIDC.ATTRIBUTES.USERNAME }}"
GITLAB_OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
GITLAB_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
GITLAB_OIDC_ISSUER: "{{ OIDC.CLIENT.ISSUER_URL }}"
GITLAB_OIDC_REDIRECT_URI: "{{ GITLAB_URL }}/users/auth/openid_connect/callback"
GITLAB_OIDC_REDIRECT_URI: "{{ (GITLAB_URL | regex_replace('/$', '')) ~ '/users/auth/openid_connect/callback' }}"
GITLAB_INIT_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
GITLAB_VERSION: "{{ lookup('config', application_id, 'services.gitlab.version') }}"
GITLAB_IMAGE: "{{ lookup('config', application_id, 'services.gitlab.image') }}"
GITLAB_CONTAINER: "{{ lookup('config', application_id, 'services.gitlab.name') }}"
GITLAB_OMNIBUS_BASE:
- "external_url '{{ GITLAB_URL }}'"
- "postgresql['enable']=false"
- "gitlab_rails['gitlab_shell_ssh_port']={{ lookup('config', application_id, 'services.gitlab.ports.public.ssh') }}"
- "gitlab_rails['db_adapter']='postgresql'"
- "gitlab_rails['db_encoding']='utf8'"
- "gitlab_rails['db_host']='{{ lookup('database', application_id, 'host') }}'"
- "gitlab_rails['db_port']='{{ lookup('database', application_id, 'port') }}'"
- "gitlab_rails['db_username']='{{ lookup('database', application_id, 'username') }}'"
- "gitlab_rails['db_password']='{{ lookup('database', application_id, 'password') }}'"
- "gitlab_rails['db_database']='{{ lookup('database', application_id, 'name') }}'"
- "gitlab_rails['db_pool']={{ POSTGRES_ALLOWED_AVG_CONNECTIONS }}"
- "nginx['listen_port']=80"
- "nginx['listen_https']=false"
- ""
- "gitlab_rails['initial_root_password']={{ GITLAB_INIT_ROOT_PASSWORD | ruby_dq }}"
- ""
- "redis['enable']=false"
- "gitlab_rails['redis_host']='{{ lookup('engine', 'redis', application_id, 'host') }}'"
- "gitlab_rails['redis_port']='{{ lookup('engine', 'redis', application_id, 'port') }}'"
- "gitlab_rails['redis_password']={{ (lookup('engine', 'redis', application_id, 'password') | ruby_dq) if lookup('engine', 'redis', application_id, 'password')
else 'nil' }}"
GITLAB_OMNIBUS_OIDC:
- ""
- "gitlab_rails['omniauth_enabled']=true"
- "gitlab_rails['omniauth_allow_single_sign_on']=['openid_connect']"
- "gitlab_rails['omniauth_block_auto_created_users']=false"
- "gitlab_rails['omniauth_auto_link_user']=['openid_connect']"
- "gitlab_rails['omniauth_providers']=[{ name: 'openid_connect', label: '{{ GITLAB_OIDC_LABEL | replace(\"'\",\"\\\\'\") }}', args: {"
- " name: 'openid_connect',"
- " scope: ['openid','profile','email'],"
- " response_type: 'code',"
- " issuer: '{{ GITLAB_OIDC_ISSUER | replace(\"'\",\"\\\\'\") }}',"
- " discovery: true,"
- " uid_field: '{{ GITLAB_OIDC_UID_FIELD | replace(\"'\",\"\\\\'\") }}',"
- " pkce: true,"
- " client_options: {"
- " identifier: '{{ GITLAB_OIDC_CLIENT_ID | replace(\"'\",\"\\\\'\") }}',"
- " secret: '{{ GITLAB_OIDC_CLIENT_SECRET | replace(\"'\",\"\\\\'\") }}',"
- " redirect_uri: '{{ GITLAB_OIDC_REDIRECT_URI | replace(\"'\",\"\\\\'\") }}'"
- " }"
- "} }]"
GITLAB_OMNIBUS_SMTP:
- ""
- "gitlab_rails['smtp_enable']=true"
- "gitlab_rails['smtp_address']={{ GITLAB_EMAIL.host | ruby_dq }}"
- "gitlab_rails['smtp_port']={{ GITLAB_EMAIL.port }}"
- "gitlab_rails['smtp_user_name']={{ GITLAB_EMAIL.username | ruby_dq }}"
- "gitlab_rails['smtp_password']={{ GITLAB_EMAIL.password | ruby_dq }}"
- "gitlab_rails['smtp_domain']={{ GITLAB_EMAIL.domain | ruby_dq }}"
- "gitlab_rails['smtp_authentication']={{ (GITLAB_EMAIL.auth | bool) | ternary(\"'login'\", 'nil') }}"
- "gitlab_rails['smtp_enable_starttls_auto']={{ GITLAB_EMAIL.start_tls | bool | string | lower }}"
- "gitlab_rails['smtp_tls']={{ GITLAB_EMAIL.tls | bool | string | lower }}"
- "gitlab_rails['gitlab_email_from']={{ GITLAB_EMAIL['from'] | ruby_dq }}"
- "gitlab_rails['gitlab_email_reply_to']={{ GITLAB_EMAIL['from'] | ruby_dq }}"
GITLAB_OBJSTORE_ENABLED: "{{ lookup('objstore', application_id, 'enabled') | bool }}"
GITLAB_OBJSTORE_BUCKET: "{{ lookup('objstore', application_id, 'bucket') }}"
@@ -86,27 +38,33 @@ GITLAB_OBJSTORE_SECRET_KEY: "{{ lookup('objstore', application_id, 'secret_key')
GITLAB_OBJSTORE_ENDPOINT: "{{ lookup('objstore', application_id, 'url') }}"
GITLAB_OBJSTORE_REGION: "{{ lookup('objstore', application_id, 'region') }}"
GITLAB_OMNIBUS_OBJSTORE:
- ""
- "gitlab_rails['object_store']['enabled']=true"
- "gitlab_rails['object_store']['proxy_download']=true"
- "gitlab_rails['object_store']['connection']={"
- " 'provider' => 'AWS',"
- " 'endpoint' => {{ GITLAB_OBJSTORE_ENDPOINT | ruby_dq }},"
- " 'region' => {{ GITLAB_OBJSTORE_REGION | ruby_dq }},"
- " 'path_style' => true,"
- " 'aws_access_key_id' => {{ GITLAB_OBJSTORE_ACCESS_KEY | ruby_dq }},"
- " 'aws_secret_access_key' => {{ GITLAB_OBJSTORE_SECRET_KEY | ruby_dq }}"
- "}"
- "gitlab_rails['object_store']['objects']['artifacts']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/artifacts') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['external_diffs']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/external-diffs') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['lfs']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/lfs') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['uploads']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/uploads') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['packages']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/packages') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['dependency_proxy']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/dependency-proxy') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['terraform_state']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/terraform-state') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['ci_secure_files']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/ci-secure-files') | ruby_dq }}"
- "gitlab_rails['object_store']['objects']['pages']['bucket']={{ (GITLAB_OBJSTORE_BUCKET ~ '/pages') | ruby_dq }}"
GITLAB_REDIS_HOST: "{{ lookup('engine', 'redis', application_id, 'host') }}"
GITLAB_REDIS_PORT: "{{ lookup('engine', 'redis', application_id, 'port') }}"
GITLAB_REDIS_PASSWORD: "{{ lookup('engine', 'redis', application_id, 'password') }}"
GITLAB_REDIS_URL: "redis://{% if GITLAB_REDIS_PASSWORD %}:{{ GITLAB_REDIS_PASSWORD | urlencode }}@{% endif %}{{ GITLAB_REDIS_HOST }}:{{ GITLAB_REDIS_PORT }}"
GITLAB_OMNIBUS_ALL: "{{ GITLAB_OMNIBUS_BASE + (GITLAB_OMNIBUS_OIDC if GITLAB_OIDC_ENABLED else []) + (GITLAB_OMNIBUS_SMTP if GITLAB_EMAIL_ENABLED else []) + (GITLAB_OMNIBUS_OBJSTORE
if GITLAB_OBJSTORE_ENABLED else []) }}"
GITLAB_SHELL_SECRET: "{{ lookup('config', application_id, 'credentials.gitlab_shell_secret') }}"
GITLAB_GITALY_TOKEN: "{{ lookup('config', application_id, 'credentials.gitaly_token') }}"
GITLAB_WORKHORSE_SECRET: "{{ lookup('config', application_id, 'credentials.workhorse_secret') }}"
GITLAB_CONFIG_DIR: "{{ lookup('container', application_id, 'directories.config') }}"
GITLAB_CONFIG_RAILS_DIR: "{{ [GITLAB_CONFIG_DIR, 'rails'] | path_join }}"
GITLAB_CONFIG_SECRETS_DIR: "{{ [GITLAB_CONFIG_DIR, 'secrets'] | path_join }}"
GITLAB_CONFIG_WORKHORSE_DIR: "{{ [GITLAB_CONFIG_DIR, 'workhorse'] | path_join }}"
GITLAB_CONFIG_SHELL_DIR: "{{ [GITLAB_CONFIG_DIR, 'shell'] | path_join }}"
GITLAB_CONFIG_GITALY_DIR: "{{ [GITLAB_CONFIG_DIR, 'gitaly'] | path_join }}"
GITLAB_CONFIG_HOSTKEYS_DIR: "{{ [GITLAB_CONFIG_DIR, 'hostkeys'] | path_join }}"
GITLAB_WEBSERVICE_SERVICE_NAME: "webservice"
GITLAB_SIDEKIQ_SERVICE_NAME: "sidekiq"
GITLAB_WORKHORSE_SERVICE_NAME: "workhorse"
GITLAB_GITALY_SERVICE_NAME: "gitaly"
GITLAB_SHELL_SERVICE_NAME: "shell"
GITLAB_MIGRATIONS_SERVICE_NAME: "migrations"
GITLAB_WEBSERVICE_CONTAINER: "{{ lookup('config', application_id, 'services.webservice.name') }}"
GITLAB_SIDEKIQ_CONTAINER: "{{ lookup('config', application_id, 'services.sidekiq.name') }}"
GITLAB_WORKHORSE_CONTAINER: "{{ lookup('config', application_id, 'services.workhorse.name') }}"
GITLAB_GITALY_CONTAINER: "{{ lookup('config', application_id, 'services.gitaly.name') }}"
GITLAB_SHELL_CONTAINER: "{{ lookup('config', application_id, 'services.shell.name') }}"
GITLAB_MIGRATIONS_CONTAINER: "{{ lookup('config', application_id, 'services.migrations.name') }}"
@@ -1,7 +1,7 @@
---
- name: 🔍 Resolve GitLab integration provisioning facts
set_fact:
nextcloud_gl_container: "{{ lookup('config', 'web-app-gitlab', 'services.gitlab.name') }}"
nextcloud_gl_container: "{{ lookup('config', 'web-app-gitlab', 'services.webservice.name') }}"
nextcloud_gl_nc_base_url: "{{ lookup('tls', 'web-app-nextcloud', 'url.base') | regex_replace('/$', '') }}"
changed_when: false
@@ -12,8 +12,10 @@
- -e
- "NC_HAS_SECRET={{ nextcloud_gl_client_secret_current.stdout | trim }}"
- "{{ nextcloud_gl_container }}" # nocheck: container-exec-resolver runs only behind the bare-name partner probe gate, compose co-location
- gitlab-rails
- /srv/gitlab/bin/rails
- runner
- -e
- production
- "-"
stdin: "{{ lookup('file', 'gitlab/provision_oauth_app.rb') }}"
register: nextcloud_gl_provision
@@ -266,6 +266,96 @@ class TestComposeVolumes(unittest.TestCase):
self.assertEqual(vol["driver_opts"]["device"], f"{_DIR_VAR_LIB}/app_images")
self.assertNotIn("nfs", vol)
def test_swarm_nfs_false_opt_out_stays_plain_local_volume(self):
apps = self._base_apps()
rendered = _call(
apps,
"app",
extra_volumes={
"repositories": {"name": "app_repositories", "nfs": False},
"shared": {"name": "app_shared"},
},
deployment_mode="swarm",
storage={
"backend": "nfs",
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
},
)
data = self._parse_yaml(rendered)
repositories = data["volumes"]["repositories"]
self.assertEqual(repositories, {"name": "app_repositories"})
shared = data["volumes"]["shared"]
self.assertEqual(shared["driver"], "local")
self.assertEqual(shared["driver_opts"]["device"], f"{_DIR_VAR_LIB}/app_shared")
def test_swarm_nfs_false_opt_out_from_meta_volumes(self):
apps = self._base_apps()
apps["app"]["volumes"] = {
"repositories": {
"type": "volume",
"name": "app_repositories",
"nfs": False,
"mounts": [{"service": "gitaly", "target": "/home/git/repositories"}],
},
"shared": {
"type": "volume",
"name": "app_shared",
"mounts": [{"service": "web", "target": "/srv/app/shared"}],
},
}
rendered = _call(
apps,
"app",
deployment_mode="swarm",
storage={
"backend": "nfs",
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
},
)
data = self._parse_yaml(rendered)
self.assertEqual(data["volumes"]["repositories"], {"name": "app_repositories"})
self.assertIn("driver_opts", data["volumes"]["shared"])
def test_swarm_nfs_dict_value_keeps_rewrite(self):
apps = self._base_apps()
apps["app"]["volumes"] = {
"data": {
"type": "volume",
"name": "app_data",
"nfs": {"uid": 1000, "gid": 1000, "mode": "0750"},
"mounts": [{"service": "web", "target": "/data"}],
},
}
rendered = _call(
apps,
"app",
deployment_mode="swarm",
storage={
"backend": "nfs",
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
},
)
data = self._parse_yaml(rendered)
vol = data["volumes"]["data"]
self.assertEqual(vol["driver_opts"]["device"], f"{_DIR_VAR_LIB}/app_data")
self.assertNotIn("nfs", vol)
def test_compose_mode_nfs_false_leaves_no_nfs_key(self):
apps = self._base_apps()
rendered = _call(
apps,
"app",
extra_volumes={"repositories": {"name": "app_repositories", "nfs": False}},
deployment_mode="compose",
storage={"backend": "local"},
)
data = self._parse_yaml(rendered)
self.assertEqual(data["volumes"]["repositories"], {"name": "app_repositories"})
def test_swarm_pinned_role_stays_node_local(self):
apps = self._base_apps()
with mock.patch(
+26 -36
View File
@@ -3,55 +3,45 @@ import unittest
from plugins.filter.ruby import FilterModule
class TestRubySingleLine(unittest.TestCase):
"""ruby_single_line collapses a multi-line Ruby program to one physical
line so a swarm `docker stack deploy` env-file (which reads embedded
newlines as separate, whitespace-named entries) keeps a value like
GITLAB_OMNIBUS_CONFIG intact: statement-boundary newlines (bracket depth 0)
become ';', newlines inside an open bracket or a string become a space.
"""
class TestRubyDq(unittest.TestCase):
"""ruby_dq escapes a value for a Ruby double-quoted string and wraps it
in double quotes; used by the gitlab smtp_settings.rb initializer."""
def setUp(self):
self.f = FilterModule().filters()["ruby_single_line"]
self.f = FilterModule().filters()["ruby_dq"]
def test_none_returns_empty(self):
self.assertEqual(self.f(None), "")
def test_none_returns_empty_quotes(self):
self.assertEqual(self.f(None), '""')
def test_single_line_unchanged(self):
self.assertEqual(self.f("a = 1"), "a = 1")
def test_empty_string(self):
self.assertEqual(self.f(""), '""')
def test_depth_zero_newline_becomes_semicolon(self):
self.assertEqual(self.f("a = 1\nb = 2"), "a = 1;b = 2")
def test_plain_value_wrapped(self):
self.assertEqual(self.f("secret"), '"secret"')
def test_newline_inside_parens_becomes_space(self):
self.assertEqual(self.f("foo(\na\n)"), "foo( a )")
def test_backslash_escaped(self):
self.assertEqual(self.f("a\\b"), '"a\\\\b"')
def test_newline_inside_brackets_becomes_space(self):
self.assertEqual(self.f("x = [\n1\n]"), "x = [ 1 ]")
def test_double_quote_escaped(self):
self.assertEqual(self.f('a"b'), '"a\\"b"')
def test_newline_inside_braces_becomes_space(self):
self.assertEqual(self.f("h = {\n}"), "h = { }")
def test_newline_escaped(self):
self.assertEqual(self.f("a\nb"), '"a\\nb"')
def test_newline_inside_string_becomes_space(self):
self.assertEqual(self.f("x = 'a\nb'"), "x = 'a b'")
def test_carriage_return_escaped(self):
self.assertEqual(self.f("a\rb"), '"a\\rb"')
def test_escaped_quote_in_string_does_not_break_depth(self):
# backslash + next char pass through verbatim, so the escaped quote
# does not terminate the string and the following newline is still a
# depth-0 statement boundary.
self.assertEqual(self.f('a = "x\\"y"\nb = 2'), 'a = "x\\"y";b = 2')
def test_tab_escaped(self):
self.assertEqual(self.f("a\tb"), '"a\\tb"')
def test_realistic_gitlab_omnibus_config(self):
src = (
"external_url 'https://gitlab.example'\ngitlab_rails['smtp_enable'] = true"
)
self.assertEqual(
self.f(src),
"external_url 'https://gitlab.example';gitlab_rails['smtp_enable'] = true",
)
def test_backslash_before_quote_order(self):
self.assertEqual(self.f('\\"'), '"\\\\\\""')
def test_dollar_and_single_quote_untouched(self):
self.assertEqual(self.f("$a'b"), '"$a\'b"')
def test_non_string_is_stringified(self):
self.assertEqual(self.f(123), "123")
self.assertEqual(self.f(123), '"123"')
if __name__ == "__main__":
@@ -2,8 +2,12 @@ import base64
import re
import unittest
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.serialization import (
Encoding,
PublicFormat,
load_pem_private_key,
)
from utils.manager.value_generator import ValueGenerator
@@ -74,6 +78,20 @@ class TestValueGenerator(unittest.TestCase):
self.assertEqual(len(point), 65)
self.assertEqual(point[0], 0x04)
def test_generate_value_rsa_pem_2048(self):
v = self.vg.generate_value("rsa_pem_2048")
self.assertTrue(v.startswith("-----BEGIN RSA PRIVATE KEY-----"))
self.assertTrue(v.rstrip().endswith("-----END RSA PRIVATE KEY-----"))
key = load_pem_private_key(v.encode(), password=None)
self.assertIsInstance(key, rsa.RSAPrivateKey)
self.assertEqual(key.key_size, 2048)
def test_generate_value_rsa_pem_2048_is_not_cached(self):
self.assertNotEqual(
self.vg.generate_value("rsa_pem_2048"),
self.vg.generate_value("rsa_pem_2048"),
)
def test_vapid_keypair_is_cached(self):
self.assertEqual(
self.vg.generate_vapid_keypair(), self.vg.generate_vapid_keypair()
+13 -2
View File
@@ -5,8 +5,13 @@ import secrets
import string
import bcrypt
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.serialization import (
Encoding,
NoEncryption,
PrivateFormat,
PublicFormat,
)
class ValueGenerator:
@@ -74,6 +79,7 @@ class ValueGenerator:
• "base64_prefixed_32"
• "vapid_private"
• "vapid_public"
• "rsa_pem_2048"
"""
if algorithm == "random_hex":
return secrets.token_hex(64)
@@ -105,4 +111,9 @@ class ValueGenerator:
return self.generate_vapid_keypair()[0]
if algorithm == "vapid_public":
return self.generate_vapid_keypair()[1]
if algorithm == "rsa_pem_2048":
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
return key.private_bytes(
Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()
).decode()
return "undefined"