diff --git a/Makefile b/Makefile index ef005b0..e80da21 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ DOCKER_RUN := docker run --rm -it \ addon-harbor addon-gitea addon-owncloud addon-nextcloud \ addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \ addon-smtp-relay addon-vault addon-external-secrets \ - addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ingress-proxypass \ + addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ingress-proxypass addon-ingress-add-domains \ add-node remove-node \ add-etcd-node remove-etcd-node \ etcd-backup etcd-restore etcd-list-snapshots \ @@ -424,6 +424,10 @@ addon-ingress-proxypass: _check_env _check_image ## Проксировать в @printf "$(CYAN)Устанавливаю External Services Ingress Proxy...$(NC)\n" $(DOCKER_RUN) addon ingress-proxypass $(ARGS) +addon-ingress-add-domains: _check_env _check_image ## Добавить домены к существующим сервисам кластера (Ingress-only) + @printf "$(CYAN)Устанавливаю ingress-add-domains...$(NC)\n" + $(DOCKER_RUN) addon ingress-add-domains $(ARGS) + # Generic цель — любой аддон из addons//playbook.yml addon-%: _check_env _check_image @if [ ! -f "addons/$*/playbook.yml" ]; then \ diff --git a/addons/ingress-add-domains/README.md b/addons/ingress-add-domains/README.md new file mode 100644 index 0000000..80108f7 --- /dev/null +++ b/addons/ingress-add-domains/README.md @@ -0,0 +1,214 @@ +# ingress-add-domains + +Добавляет дополнительные домены к уже существующим сервисам внутри кластера. + +**Не создаёт** Service или Endpoints — только Ingress, указывающий на готовый K8s Service. +Каждый Ingress создаётся **в namespace целевого сервиса**, что является требованием Kubernetes. + +Поддерживает: +- Внутренние домены (`*.local`) без TLS +- Внешние домены (`*.home.ru`) с TLS и cert-manager +- Basic auth (логин/пароль — хэш генерируется автоматически) +- WebSocket +- Per-entry переопределение любых параметров + +## Когда использовать + +| Сценарий | Инструмент | +|---|---| +| Открыть сервис кластера по новому домену | **ingress-add-domains** | +| Проксировать сервис вне кластера (по IP) | [ingress-proxypass](../ingress-proxypass/README.md) | + +## Быстрый старт + +```yaml +# group_vars/all/addons.yml +addon_ingress_add_domains: true + +ingress_add_domains_entries: + # Внутренний домен без TLS + - name: gitea-local + hosts: [gitea.local] + service: + name: gitea-http + namespace: gitea + port: 3000 + + # Внешний домен с TLS + basic auth + - name: gitea-home + hosts: [gitea.home.ru] + service: + name: gitea-http + namespace: gitea + port: 3000 + tls: + enabled: true + certManager: + enabled: true + issuer: letsencrypt-prod + issuerKind: ClusterIssuer + auth: + enabled: true + username: admin + password: "{{ vault_gitea_proxy_password }}" +``` + +```bash +make addon-ingress-add-domains +``` + +## Конфигурация + +### Переменные Ansible + +| Переменная | По умолчанию | Описание | +|---|---|---| +| `ingress_add_domains_release_name` | `ingress-add-domains` | Имя Helm release | +| `ingress_add_domains_release_namespace` | `ingress-add-domains` | Namespace для Helm-метаданных | +| `ingress_add_domains_defaults` | см. ниже | Глобальные умолчания | +| `ingress_add_domains_entries` | `[]` | Список записей | + +### Поля entry + +| Поле | Обязательно | Описание | +|---|---|---| +| `name` | да | Уникальное имя (становится именем Ingress-ресурса) | +| `hosts` | да | Список доменов | +| `service.name` | да | Имя существующего K8s Service | +| `service.namespace` | да | Namespace сервиса (Ingress создаётся в этом namespace) | +| `service.port` | да | Порт сервиса | +| `tls` | нет | Настройки TLS (см. ниже) | +| `auth` | нет | Basic auth (см. ниже) | +| `websocket` | нет | Включить поддержку WebSocket (`true`/`false`) | +| `path` | нет | URL-путь (по умолчанию `/`) | +| `pathType` | нет | `Prefix` или `Exact` (по умолчанию `Prefix`) | +| `ingressClass` | нет | Класс ingress (по умолчанию `nginx`) | +| `annotations` | нет | Дополнительные аннотации (перекрывают всё) | + +### TLS + +```yaml +tls: + enabled: true + secretName: wildcard-tls # использовать готовый Secret (опционально) + certManager: + enabled: true # автоматически создать Certificate + issuer: letsencrypt-prod + issuerKind: ClusterIssuer # или Issuer +``` + +### Basic auth + +```yaml +auth: + enabled: true + username: admin + password: "{{ vault_password }}" # хэш генерируется автоматически + + # ИЛИ готовая htpasswd-строка: + # credentials: "admin:$apr1$..." + + # ИЛИ использовать существующий Secret: + # secretName: my-auth-secret +``` + +Если задан `username` + `password` — Ansible автоматически хэширует пароль через +`openssl passwd -apr1` и создаёт K8s Secret. Открытый пароль не попадает в Helm values. + +### Глобальные умолчания + +```yaml +ingress_add_domains_defaults: + ingressClass: nginx + tls: + enabled: false + certManager: + enabled: false + issuer: "" + issuerKind: ClusterIssuer + auth: + enabled: false + websocket: false + path: / + pathType: Prefix + annotations: {} +``` + +Любое поле defaults можно переопределить на уровне entry. + +## Примеры + +### Несколько доменов для одного сервиса + +```yaml +ingress_add_domains_entries: + - name: nextcloud-all-domains + hosts: + - nextcloud.local + - cloud.local + - nextcloud.home.ru + service: + name: nextcloud + namespace: nextcloud + port: 8080 + tls: + enabled: true + secretName: wildcard-home-tls +``` + +### Wildcard TLS Secret для всех записей + +```yaml +ingress_add_domains_defaults: + tls: + enabled: true + secretName: wildcard-home-tls + +ingress_add_domains_entries: + - name: gitea-home + hosts: [gitea.home.ru] + service: {name: gitea-http, namespace: gitea, port: 3000} + + - name: harbor-home + hosts: [harbor.home.ru] + service: {name: harbor-core, namespace: harbor, port: 80} + tls: + secretName: harbor-specific-tls # перекрывает default +``` + +### Basic auth для всех записей + +```yaml +ingress_add_domains_defaults: + auth: + enabled: true + username: admin + password: "{{ vault_proxy_password }}" + +ingress_add_domains_entries: + - name: argocd-home + hosts: [argocd.home.ru] + service: {name: argocd-server, namespace: argocd, port: 80} + + - name: grafana-home + hosts: [grafana.home.ru] + service: {name: prometheus-stack-grafana, namespace: monitoring, port: 80} + auth: + enabled: false # отключить auth для конкретной записи +``` + +## Диагностика + +```bash +# Посмотреть все созданные Ingresses +kubectl get ingress -A -l app.kubernetes.io/instance=ingress-add-domains + +# Детали конкретного Ingress +kubectl describe ingress -n + +# Логи ingress-nginx +kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --tail=50 + +# Проверить auth Secret +kubectl -n get secret -auth +``` diff --git a/addons/ingress-add-domains/playbook.yml b/addons/ingress-add-domains/playbook.yml new file mode 100644 index 0000000..15e1994 --- /dev/null +++ b/addons/ingress-add-domains/playbook.yml @@ -0,0 +1,7 @@ +--- +- name: Install ingress-add-domains + hosts: k3s_master[0] + gather_facts: false + become: true + roles: + - role: "{{ playbook_dir }}/role" diff --git a/addons/ingress-add-domains/role/chart/Chart.yaml b/addons/ingress-add-domains/role/chart/Chart.yaml new file mode 100644 index 0000000..d015399 --- /dev/null +++ b/addons/ingress-add-domains/role/chart/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: ingress-add-domains +description: | + Добавляет дополнительные домены (Ingress) к уже существующим сервисам внутри кластера. + Не создаёт Service/Endpoints — только Ingress, указывающий на готовый K8s Service. + Каждый Ingress создаётся в namespace целевого сервиса. + Поддерживает: TLS (готовый Secret или cert-manager), basic auth, WebSocket, несколько хостов. +type: application +version: 1.0.0 +appVersion: "1.0.0" +keywords: + - ingress + - domains + - nginx +home: https://git.antropoff.ru/DevOpsTools/K3S diff --git a/addons/ingress-add-domains/role/chart/templates/NOTES.txt b/addons/ingress-add-domains/role/chart/templates/NOTES.txt new file mode 100644 index 0000000..045b142 --- /dev/null +++ b/addons/ingress-add-domains/role/chart/templates/NOTES.txt @@ -0,0 +1,12 @@ +ingress-add-domains deployed successfully. + +Ingresses created (each in the namespace of its target Service): +{{- range .Values.entries }} +{{- $svc := .service }} + • {{ include "ingress-add-domains.resourceName" .name }} (namespace: {{ $svc.namespace }}) + hosts: {{ .hosts | default (list (.host | default "")) | join ", " }} + → {{ $svc.name }}:{{ $svc.port }} +{{- end }} + +Verify: + kubectl get ingress -A -l app.kubernetes.io/instance={{ .Release.Name }} diff --git a/addons/ingress-add-domains/role/chart/templates/_helpers.tpl b/addons/ingress-add-domains/role/chart/templates/_helpers.tpl new file mode 100644 index 0000000..6081404 --- /dev/null +++ b/addons/ingress-add-domains/role/chart/templates/_helpers.tpl @@ -0,0 +1,24 @@ +{{/* +Normalize an entry name to a safe Kubernetes resource name. +Usage: {{ include "ingress-add-domains.resourceName" "my_service.name" }} +*/}} +{{- define "ingress-add-domains.resourceName" -}} +{{- . | lower | replace "_" "-" | replace "." "-" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Chart label string. +*/}} +{{- define "ingress-add-domains.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels applied to all resources. +*/}} +{{- define "ingress-add-domains.labels" -}} +helm.sh/chart: {{ include "ingress-add-domains.chart" . }} +app.kubernetes.io/name: {{ default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/addons/ingress-add-domains/role/chart/templates/ingress.yaml b/addons/ingress-add-domains/role/chart/templates/ingress.yaml new file mode 100644 index 0000000..343c862 --- /dev/null +++ b/addons/ingress-add-domains/role/chart/templates/ingress.yaml @@ -0,0 +1,122 @@ +{{/* +Creates one Ingress per entry, placed in the SAME namespace as the target Service. +metadata.namespace is set explicitly so each Ingress lives next to its backend. + +Feature resolution order (highest → lowest priority): + 1. Per-entry annotations (.entries[*].annotations) — override everything + 2. Per-entry feature flags (tls, auth, websocket, …) + 3. Global defaults (.defaults.*) + 4. Built-in generated annotations (ingressClass, auth secret pointer) +*/}} +{{- range .Values.entries }} +{{- $entry := . }} +{{- $d := $.Values.defaults }} +{{- $entryName := include "ingress-add-domains.resourceName" $entry.name }} +{{- $svc := $entry.service }} + +{{/* ── Resolve settings with fallback to defaults ─────────────────────────── */}} +{{- $ingressClass := $entry.ingressClass | default $d.ingressClass | default "nginx" }} +{{- $path := $entry.path | default $d.path | default "/" }} +{{- $pathType := $entry.pathType | default $d.pathType | default "Prefix" }} + +{{/* websocket: nil-safe three-way check */}} +{{- $websocket := false }} +{{- if ne ($entry.websocket | toString) "" }} +{{- $websocket = $entry.websocket }} +{{- else if ne ($d.websocket | toString) "" }} +{{- $websocket = $d.websocket }} +{{- end }} + +{{/* ── TLS ──────────────────────────────────────────────────────────────────── */}} +{{- $entryTLS := $entry.tls | default dict }} +{{- $defTLS := $d.tls | default dict }} +{{- $entryCM := $entryTLS.certManager | default dict }} +{{- $defCM := $defTLS.certManager | default dict }} +{{- $tlsEnabled := $entryTLS.enabled | default $defTLS.enabled | default false }} +{{- $tlsSecret := $entryTLS.secretName | default $defTLS.secretName | default "" }} +{{- $cmEnabled := $entryCM.enabled | default $defCM.enabled | default false }} +{{- $cmIssuer := $entryCM.issuer | default $defCM.issuer | default "" }} +{{- $cmKind := $entryCM.issuerKind | default $defCM.issuerKind | default "ClusterIssuer" }} + +{{/* ── Auth ─────────────────────────────────────────────────────────────────── */}} +{{- $entryAuth := $entry.auth | default dict }} +{{- $defAuth := $d.auth | default dict }} +{{- $authEnabled := $entryAuth.enabled | default $defAuth.enabled | default false }} +{{- $authSecret := "" }} +{{- if $authEnabled }} + {{- $authSecret = $entryAuth.secretName | default $defAuth.secretName | default (printf "%s-auth" $entryName) }} +{{- end }} + +{{/* ── Hosts ────────────────────────────────────────────────────────────────── */}} +{{- $hosts := $entry.hosts | default (list ($entry.host | default "")) }} + +{{/* ── Build annotation dict ───────────────────────────────────────────────── */}} +{{- $ann := dict }} + +{{/* Step 1: global default annotations (lowest priority) */}} +{{- range $k, $v := ($d.annotations | default dict) }} + {{- $_ := set $ann $k ($v | toString) }} +{{- end }} + +{{/* Step 2: WebSocket */}} +{{- if $websocket }} + {{- $_ := set $ann "nginx.ingress.kubernetes.io/proxy-http-version" "1.1" }} +{{- end }} + +{{/* Step 3: basic auth */}} +{{- if $authEnabled }} + {{- $_ := set $ann "nginx.ingress.kubernetes.io/auth-type" "basic" }} + {{- $_ := set $ann "nginx.ingress.kubernetes.io/auth-secret" $authSecret }} + {{- $_ := set $ann "nginx.ingress.kubernetes.io/auth-realm" "Authentication Required" }} +{{- end }} + +{{/* Step 4: cert-manager */}} +{{- if $cmEnabled }} + {{- $cmAnnotationKey := printf "cert-manager.io/%s" ($cmKind | lower) }} + {{- $_ := set $ann $cmAnnotationKey $cmIssuer }} +{{- end }} + +{{/* Step 5: per-entry custom annotations override everything above */}} +{{- range $k, $v := ($entry.annotations | default dict) }} + {{- $_ := set $ann $k ($v | toString) }} +{{- end }} + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $entryName }} + namespace: {{ $svc.namespace }} + labels: + {{- include "ingress-add-domains.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $entryName }} + {{- if $ann }} + annotations: + {{- toYaml $ann | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ $ingressClass }} + {{- if $tlsEnabled }} + tls: + - hosts: + {{- range $hosts }} + - {{ . | quote }} + {{- end }} + {{- if $tlsSecret }} + secretName: {{ $tlsSecret | quote }} + {{- end }} + {{- end }} + rules: + {{- range $hosts }} + - host: {{ . | quote }} + http: + paths: + - path: {{ $path | quote }} + pathType: {{ $pathType }} + backend: + service: + name: {{ $svc.name }} + port: + number: {{ $svc.port }} + {{- end }} +{{- end }} diff --git a/addons/ingress-add-domains/role/chart/templates/secret-auth.yaml b/addons/ingress-add-domains/role/chart/templates/secret-auth.yaml new file mode 100644 index 0000000..e1824a9 --- /dev/null +++ b/addons/ingress-add-domains/role/chart/templates/secret-auth.yaml @@ -0,0 +1,38 @@ +{{/* +Creates a basic-auth Secret for each entry that has: + auth.enabled: true + auth.credentials: "" (no auth.secretName — use existing instead) + +The Secret is created in the SAME namespace as the target Service so that +nginx can read it (Ingress and Secret must be in the same namespace). + +The Secret key MUST be "auth" for nginx's auth-file type. +*/}} +{{- range .Values.entries }} +{{- $entry := . }} +{{- $d := $.Values.defaults }} +{{- $entryName := include "ingress-add-domains.resourceName" $entry.name }} +{{- $svc := $entry.service }} + +{{- $entryAuth := $entry.auth | default dict }} +{{- $defAuth := $d.auth | default dict }} +{{- $authEnabled := $entryAuth.enabled | default $defAuth.enabled | default false }} +{{- $existingSec := $entryAuth.secretName | default $defAuth.secretName | default "" }} +{{- $credentials := $entryAuth.credentials | default $defAuth.credentials | default "" }} + +{{/* Only create a Secret when auth is on, no existing secret is referenced, and credentials are provided */}} +{{- if and $authEnabled (not $existingSec) $credentials }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $entryName }}-auth + namespace: {{ $svc.namespace }} + labels: + {{- include "ingress-add-domains.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $entryName }} +type: Opaque +data: + auth: {{ $credentials | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/addons/ingress-add-domains/role/chart/values.yaml b/addons/ingress-add-domains/role/chart/values.yaml new file mode 100644 index 0000000..928e316 --- /dev/null +++ b/addons/ingress-add-domains/role/chart/values.yaml @@ -0,0 +1,23 @@ +# Default values — override via group_vars/all/addons.yml → ingress_add_domains_* + +defaults: + ingressClass: nginx + tls: + enabled: false + secretName: "" + certManager: + enabled: false + issuer: "" + issuerKind: ClusterIssuer + auth: + enabled: false + credentials: "" # htpasswd string: user:$apr1$... + secretName: "" # use existing Secret instead + websocket: false + path: / + pathType: Prefix + annotations: {} + +# Each entry creates one Ingress in the same namespace as the target Service. +# See README.md for full field reference. +entries: [] diff --git a/addons/ingress-add-domains/role/defaults/main.yml b/addons/ingress-add-domains/role/defaults/main.yml new file mode 100644 index 0000000..8936db2 --- /dev/null +++ b/addons/ingress-add-domains/role/defaults/main.yml @@ -0,0 +1,61 @@ +--- +# ─── Helm release ───────────────────────────────────────────────────────────── +ingress_add_domains_release_name: "ingress-add-domains" +ingress_add_domains_release_namespace: "ingress-add-domains" + +# ─── Global defaults (mirror of chart values.defaults) ──────────────────────── +ingress_add_domains_defaults: + ingressClass: nginx + tls: + enabled: false + secretName: "" + certManager: + enabled: false + issuer: "" + issuerKind: ClusterIssuer + auth: + enabled: false + username: "" # логин — хэшируется автоматически через openssl passwd -apr1 + password: "" # пароль в открытом виде (задай в vault.yml!) + credentials: "" # готовая htpasswd-строка (если задана — username/password игнорируются) + secretName: "" # использовать существующий Secret вместо генерации нового + websocket: false + path: / + pathType: Prefix + annotations: {} + +# ─── Entry definitions ──────────────────────────────────────────────────────── +# Каждая запись создаёт один Ingress в namespace целевого сервиса. +# Все поля поддерживают per-entry переопределение ingress_add_domains_defaults. +# +# Минимальный пример (*.local без TLS): +# ingress_add_domains_entries: +# - name: gitea-local +# hosts: [gitea.local] +# service: +# name: gitea-http +# namespace: gitea +# port: 3000 +# +# Полный пример (*.home.ru с TLS + basic auth): +# ingress_add_domains_entries: +# - name: gitea-home +# hosts: +# - gitea.home.ru +# service: +# name: gitea-http +# namespace: gitea +# port: 3000 +# tls: +# enabled: true +# certManager: +# enabled: true +# issuer: letsencrypt-prod +# issuerKind: ClusterIssuer +# auth: +# enabled: true +# username: admin +# password: "{{ vault_gitea_proxy_password }}" +# annotations: +# nginx.ingress.kubernetes.io/proxy-body-size: "0" +ingress_add_domains_entries: [] diff --git a/addons/ingress-add-domains/role/tasks/main.yml b/addons/ingress-add-domains/role/tasks/main.yml new file mode 100644 index 0000000..9532e29 --- /dev/null +++ b/addons/ingress-add-domains/role/tasks/main.yml @@ -0,0 +1,176 @@ +--- +# ── Validate inputs ─────────────────────────────────────────────────────────── + +- name: Validate ingress_add_domains_entries is defined and non-empty + ansible.builtin.assert: + that: + - ingress_add_domains_entries is defined + - ingress_add_domains_entries | length > 0 + fail_msg: > + ingress_add_domains_entries is empty. Define at least one entry in + group_vars/all/addons.yml → ingress_add_domains_entries. + success_msg: "ingress_add_domains_entries: {{ ingress_add_domains_entries | length }} entry/entries defined" + +# ── Create Helm release namespace ───────────────────────────────────────────── + +- name: Create ingress-add-domains namespace + ansible.builtin.command: > + k3s kubectl create namespace {{ ingress_add_domains_release_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + changed_when: false + +# ── Copy Helm chart to master node ─────────────────────────────────────────── + +- name: Ensure chart temp directory is clean + ansible.builtin.file: + path: /tmp/ingress-add-domains-chart + state: absent + become: true + +- name: Create chart temp directory + ansible.builtin.file: + path: /tmp/ingress-add-domains-chart + state: directory + mode: "0755" + become: true + +- name: Copy Helm chart to master + ansible.builtin.copy: + src: "{{ role_path }}/chart/" + dest: /tmp/ingress-add-domains-chart/ + mode: preserve + become: true + +# ── Generate htpasswd hashes from plain username/password ──────────────────── +# Если auth.username + auth.password заданы — автоматически хэшируем через +# openssl passwd -apr1, записываем в auth.credentials и убираем открытые поля. +# Пароли в открытом виде нигде не попадают в Helm values или логи. + +- name: Generate htpasswd credentials (username/password → apr1 hash) + ansible.builtin.command: + argv: + - python3 + - -c + - | + import json, subprocess, sys + + entries = json.loads(sys.argv[1]) + def_auth = json.loads(sys.argv[2]) + + def gen_credentials(auth, fallback): + username = auth.get('username') or fallback.get('username', '') + password = auth.get('password') or fallback.get('password', '') + credentials = auth.get('credentials') or fallback.get('credentials', '') + secret_name = auth.get('secretName') or fallback.get('secretName', '') + if credentials or secret_name: + return credentials + if username and password: + r = subprocess.run( + ['openssl', 'passwd', '-apr1', password], + capture_output=True, text=True, check=True + ) + return username + ':' + r.stdout.strip() + return '' + + cleaned_def_auth = {k: v for k, v in def_auth.items() + if k not in ('username', 'password')} + creds = gen_credentials(def_auth, {}) + if creds: + cleaned_def_auth['credentials'] = creds + + for entry in entries: + auth = dict(entry.get('auth') or {}) + enabled = auth.get('enabled', def_auth.get('enabled', False)) + if enabled: + creds = gen_credentials(auth, def_auth) + if creds: + auth['credentials'] = creds + auth.pop('username', None) + auth.pop('password', None) + entry['auth'] = auth + + print(json.dumps({'entries': entries, 'def_auth': cleaned_def_auth})) + - "{{ ingress_add_domains_entries | to_json }}" + - "{{ ingress_add_domains_defaults.auth | to_json }}" + register: _auth_processed + changed_when: false + no_log: true + +- name: Set final entries and defaults with generated credentials + ansible.builtin.set_fact: + _ingress_add_domains_entries_final: "{{ (_auth_processed.stdout | from_json).entries }}" + _ingress_add_domains_def_auth_final: "{{ (_auth_processed.stdout | from_json).def_auth }}" + +# ── Template Helm values ────────────────────────────────────────────────────── + +- name: Template Helm values + ansible.builtin.template: + src: values.yaml.j2 + dest: /tmp/ingress-add-domains-values.yaml + mode: "0640" + become: true + +- name: Show generated Helm values + ansible.builtin.command: cat /tmp/ingress-add-domains-values.yaml + become: true + changed_when: false + register: _ingress_add_domains_values + +- name: Debug generated values + ansible.builtin.debug: + var: _ingress_add_domains_values.stdout_lines + +# ── Lint chart before deploying ─────────────────────────────────────────────── + +- name: Lint Helm chart + ansible.builtin.command: > + helm lint /tmp/ingress-add-domains-chart + --values /tmp/ingress-add-domains-values.yaml + become: true + changed_when: false + register: _helm_lint + failed_when: _helm_lint.rc != 0 + +# ── Deploy chart ────────────────────────────────────────────────────────────── + +- name: Deploy ingress-add-domains via Helm + ansible.builtin.command: > + helm upgrade --install {{ ingress_add_domains_release_name }} + /tmp/ingress-add-domains-chart + --namespace {{ ingress_add_domains_release_namespace }} + --values /tmp/ingress-add-domains-values.yaml + --atomic + --wait + --timeout 60s + become: true + register: _helm_result + changed_when: true + +# ── Verify deployment ───────────────────────────────────────────────────────── + +- name: Get all created Ingresses + ansible.builtin.command: > + k3s kubectl get ingress -A + -l app.kubernetes.io/instance={{ ingress_add_domains_release_name }} + -o wide + become: true + changed_when: false + register: _ingress_list + +# ── Summary ─────────────────────────────────────────────────────────────────── + +- name: "=== ingress-add-domains Ready ===" + ansible.builtin.debug: + msg: + - "╔══════════════════════════════════════════════════════════════╗" + - "║ ingress-add-domains — Deployed ║" + - "╚══════════════════════════════════════════════════════════════╝" + - "" + - " Release : {{ ingress_add_domains_release_name }}" + - " Entries : {{ ingress_add_domains_entries | length }}" + - "" + - " Ingress resources:" + - "{{ _ingress_list.stdout_lines | to_yaml }}" + - "" + - " Verify: kubectl get ingress -A -l app.kubernetes.io/instance={{ ingress_add_domains_release_name }}" diff --git a/addons/ingress-add-domains/role/templates/values.yaml.j2 b/addons/ingress-add-domains/role/templates/values.yaml.j2 new file mode 100644 index 0000000..c3dcd6b --- /dev/null +++ b/addons/ingress-add-domains/role/templates/values.yaml.j2 @@ -0,0 +1,9 @@ +# Generated by Ansible — do not edit manually. +# Configure via: group_vars/all/addons.yml → ingress_add_domains_* variables. +# Note: auth.username/password are resolved to htpasswd hashes before this file is written. + +defaults: +{{ (ingress_add_domains_defaults | combine({'auth': _ingress_add_domains_def_auth_final})) | to_yaml | indent(2, True) }} + +entries: +{{ _ingress_add_domains_entries_final | to_yaml | indent(2, True) }} diff --git a/docs/addons.md b/docs/addons.md index e2070b4..de86bc3 100644 --- a/docs/addons.md +++ b/docs/addons.md @@ -68,6 +68,7 @@ make addon-netbird | **Сеть / VPN** | | | | | splitgw | `addon_splitgw` | Прозрачный split-tunnel gateway: sing-box + Hysteria2 TPROXY, YouTube→прокси, RU→прямой | [→](../addons/splitgw/README.md) | | ingress-proxypass | `addon_ingress_proxypass` | Проксировать внешние сервисы (IP:PORT) через ingress-nginx по домену — Service + Endpoints + Ingress | [→](../addons/ingress-proxypass/README.md) | +| ingress-add-domains | `addon_ingress_add_domains` | Добавить домены к существующим K8s сервисам — только Ingress, без Service/Endpoints | [→](../addons/ingress-add-domains/README.md) | ## Конфигурация addons.yml @@ -120,6 +121,7 @@ addon_splitgw: false # sing-box + Hysteria2 TPROXY (host или k8 # ── External Services Ingress Proxy ─────────────────────────────────────────── addon_ingress_proxypass: false # проксировать внешние сервисы через ingress-nginx +addon_ingress_add_domains: false # добавить домены к существующим K8s сервисам (только Ingress) ``` ## Зависимости между аддонами @@ -211,6 +213,46 @@ ingress_proxypass_proxies: --- +## Ingress Add Domains + +Добавляет дополнительные домены к уже существующим K8s сервисам. Создаёт **только Ingress** в namespace целевого сервиса — Service и Endpoints не трогает. Поддерживает TLS с cert-manager, basic auth (автохэш пароля), WebSocket. + +```bash +make addon-ingress-add-domains +``` + +Конфигурация в `group_vars/all/addons.yml`: + +```yaml +ingress_add_domains_entries: + - name: gitea-local + hosts: [gitea.local] + service: + name: gitea-http + namespace: gitea + port: 3000 + + - name: gitea-home + hosts: [gitea.home.ru] + service: + name: gitea-http + namespace: gitea + port: 3000 + tls: + enabled: true + certManager: + enabled: true + issuer: letsencrypt-prod + auth: + enabled: true + username: admin + password: "{{ vault_gitea_password }}" +``` + +Подробнее: [addons/ingress-add-domains/README.md](../addons/ingress-add-domains/README.md) + +--- + ## Split Gateway Прозрачный split-tunnel proxy на базе sing-box с Hysteria2 как outbound. Перехватывает трафик с TV/устройств через TPROXY и маршрутизирует по правилам: YouTube → Hysteria2, RU-сервисы и частные сети → прямой маршрут. diff --git a/docs/make-reference.md b/docs/make-reference.md index df52ca2..e2f5417 100644 --- a/docs/make-reference.md +++ b/docs/make-reference.md @@ -81,6 +81,8 @@ make addon-splitgw ARGS="-e splitgw_deploy_mode=k8s" # K8s DaemonSet make addon-ingress-proxypass make addon-ingress-proxypass ARGS="-e ingress_proxypass_vip=192.168.1.x" + +make addon-ingress-add-domains ``` ## Управление нодами diff --git a/docs/networking.md b/docs/networking.md index ba5c972..ab631e8 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -127,6 +127,52 @@ netbird up --management-url grpc://netbird.example.com:80 --- +## ingress-add-domains — добавить домены к сервисам кластера + +Позволяет быстро открыть дополнительные домены для уже существующих K8s сервисов без изменения их шаблонов. Создаёт **только Ingress** в namespace целевого сервиса. Подробнее: [addons/ingress-add-domains/README.md](../addons/ingress-add-domains/README.md). + +```yaml +# group_vars/all/addons.yml +addon_ingress_add_domains: true + +ingress_add_domains_entries: + # Внутренний домен без TLS + - name: gitea-local + hosts: [gitea.local] + service: + name: gitea-http + namespace: gitea + port: 3000 + + # Внешний домен с TLS + basic auth + - name: grafana-home + hosts: [grafana.home.ru] + service: + name: prometheus-stack-grafana + namespace: monitoring + port: 80 + tls: + enabled: true + certManager: + enabled: true + issuer: letsencrypt-prod + auth: + enabled: true + username: admin + password: "{{ vault_grafana_proxy_password }}" +``` + +```bash +make addon-ingress-add-domains +``` + +| Сценарий | Инструмент | +|---|---| +| Открыть K8s-сервис по новому домену | **ingress-add-domains** | +| Проксировать сервис вне кластера (по IP) | ingress-proxypass | + +--- + ## ingress-proxypass — проксировать внешние сервисы Позволяет открыть по домену любой сервис, работающий **вне кластера** (роутер, NAS, Plex на отдельной машине и т.д.), через тот же ingress-nginx VIP. Для каждой записи создаётся: `Service (ClusterIP, без selector)` + `Endpoints` + `Ingress`. Подробнее: [addons/ingress-proxypass/README.md](../addons/ingress-proxypass/README.md). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f3db236..df98479 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -27,6 +27,8 @@ | NetBird: клиент не подключается | DNS не настроен | Настрой A-записи для Management, Signal, Coturn LB IPs | | MinIO: distributed не поднимается | Мало нод | Distributed требует минимум 4 реплики/ноды | | Nextcloud: redirect loop | HTTP→HTTPS конфликт | Добавь `TRUSTED_PROXIES` в конфиг Nextcloud | +| ingress-add-domains: 404 | Ingress не найден в namespace сервиса | `kubectl get ingress -A -l app.kubernetes.io/instance=ingress-add-domains` | +| ingress-add-domains: 503 | Сервис/порт неверный | `kubectl -n get svc ` — проверь имя и порт | | ingress-proxypass: 502 Bad Gateway | Кластер не достигает внешний IP | `curl -v http://:` с ноды кластера; проверь файрвол на внешнем хосте | | ingress-proxypass: 503 | Endpoints пустой | `kubectl -n ingress-proxypass get endpoints` — должны быть адреса | | ingress-proxypass: 404 | Имя хоста не совпадает | `kubectl -n ingress-proxypass describe ingress ` — хост должен совпасть точно | @@ -134,6 +136,22 @@ kubectl describe certificate -n kubectl logs -n cert-manager deployment/cert-manager -f ``` +## Диагностика ingress-add-domains + +```bash +# Все созданные Ingresses +kubectl get ingress -A -l app.kubernetes.io/instance=ingress-add-domains + +# Детали конкретного Ingress +kubectl describe ingress -n + +# Проверить что сервис существует +kubectl -n get svc + +# Логи nginx-контроллера +kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --tail=50 +``` + ## Диагностика ingress-proxypass ```bash diff --git a/group_vars/all/addons.yml b/group_vars/all/addons.yml index 34fefce..ee73c32 100644 --- a/group_vars/all/addons.yml +++ b/group_vars/all/addons.yml @@ -42,6 +42,7 @@ addon_mediaserver: false # MediaServer — Plex, *arr, Transmission, P addon_hysteria2_server: false # Hysteria2 VPN сервер на удалённый VPS (группа [hysteria2_server] в inventory) addon_splitgw: false # Split Gateway — прозрачный прокси sing-box+Hysteria2 (группа [splitgw] в inventory) addon_ingress_proxypass: false # External Services Ingress Proxy — проксировать внешние сервисы через ingress-nginx +addon_ingress_add_domains: false # Ingress-only — добавить домены к существующим сервисам кластера # ─── NFS Server ─────────────────────────────────────────────────────────────── nfs_exports: diff --git a/playbooks/addons.yml b/playbooks/addons.yml index a088bd8..6244e0c 100644 --- a/playbooks/addons.yml +++ b/playbooks/addons.yml @@ -303,3 +303,11 @@ when: addon_ingress_proxypass | default(false) | bool roles: - role: "{{ playbook_dir }}/../addons/ingress-proxypass/role" + +- name: Install Ingress Add Domains + hosts: k3s_master[0] + gather_facts: false + become: true + when: addon_ingress_add_domains | default(false) | bool + roles: + - role: "{{ playbook_dir }}/../addons/ingress-add-domains/role"