Compare commits

..
Author SHA1 Message Date
hermes c2c9b273f2 fix(ai): mcp-k8s — containers/kubernetes-mcp-server вместо mkp
Flate / Flate - Success (pull_request) Blocked by required conditions
Flate / Flate - Filter (pull_request) Successful in 5s
Flate / Flate (pull_request) Successful in 17s
У mkp нет ни одного безопасного режима, только RBAC. У containers/kubernetes-mcp-server есть
штатный --read-only, --toolsets и denied_resources, официальный гайд по read-only SA и чарт.
Образ quay.io/containers/kubernetes_mcp_server:v0.0.66 с дайджестом (тег проверен в API quay).
RBAC: вместо самодельного списка — встроенная view (secrets в неё не входят) + отдельный
ClusterRole на кластерные объекты и метрики. Аргументы: --port 8080 --read-only --toolsets core.
2026-09-17 23:17:35 +00:00
hermes c4cf3ba798 feat(ai): mcp-k8s через ToolHive (read-only RBAC)
Flate / Flate - Filter (pull_request) Successful in 5s
Flate / Flate (pull_request) Successful in 18s
Flate / Flate - Success (pull_request) Canceled after 0s
MCP-сервер k8s из реестра ToolHive: ghcr.io/stackloklabs/mkp/server:0.4.3, transport streamable-http.
Отдельный SA mcp-k8s и ClusterRole mcp-k8s-readonly (только get/list/watch).
Core-группа перечислена поимённо, чтобы не открывать secrets; остальные группы явным списком,
потому что apiGroups: [*] включает и core-группу. Наружу не выводим: без oidcConfigRef прокси
ToolHive неаутентифицирован.
2026-09-17 23:10:17 +00:00
hermes 0243dbfb9c feat(database): dragonfly-operator в ns database
Flate / Flate - Success (pull_request) Blocked by required conditions
Flate / Flate - Filter (pull_request) Successful in 6s
Flate / Flate (pull_request) Successful in 17s
Официальный чарт oci://ghcr.io/dragonflydb/dragonfly-operator/helm/dragonfly-operator v1.6.1.
CRD (dragonflydb.io) ставит сам чарт шаблоном templates/crds.yaml, crds.keep: true.
kube-rbac-proxy отключён (метрики на :8080), ServiceMonitor и дашборд Grafana не включаем
— Prometheus Operator в кластере нет. Образ взят из ghcr и зафиксирован дайджестом.
Dragonfly CR сюда не входит: экземпляр БД создаётся в ns того, кому он нужен.
2026-09-17 21:31:47 +00:00
hermesandgrachevko 5b33310a44 feat(ai): toolhive-operator и его CRD в ns ai 2026-09-17 21:18:29 +00:00
hermesandgrachevko 12ad1c040c chore(security): поправить комментарий про dependsOn в ESO (#12)
Одна задача = один MR: правка только комментария, поведение не меняется.

**Что было не так**

Комментарий утверждал, что `dependsOn` работает только внутри своего ns. Это неверно и вводит в заблуждение.

**Как на самом деле**

По CRD-схеме kustomize-controller (`api/v1/dependsOn[].namespace`):

```
namespace:
  description: |-
    Namespace of the referent, defaults to the namespace of the resource
    object that contains the reference.
```

То есть поле опционально и по умолчанию равно namespace **самого объекта Kustomization**, а не namespace, куда тот раскидывает ресурсы. Поэтому «зависимости из другого ns» бывают двух разных видов:

- в `ewatkins/talos-cluster` все app-Kustomization лежат в `flux-system` (`apps/default/netbox/ks.yaml`: `namespace: flux-system`, `targetNamespace: default`), поэтому `dependsOn` на `dragonfly` и `crunchy-postgres-operator-*` пишется без поля `namespace`, хотя рабочие нагрузки уезжают в другие ns;
- в `drag0n141/home-ops` Kustomization лежат в своём target-ns (`apps/ai/toolhive/ks.yaml`: `namespace: &namespace ai`), и там кросс-ns зависимости пишутся явно: `onepassword-connect` с `namespace: external-secrets`, `pocket-id-instance` с `namespace: security`.

**Что у нас**

Наш репозиторий следует второму варианту: `kubernetes/apps/<ns>/kustomization.yaml` задаёт `namespace: <ns>`, то есть app-Kustomization живут в своём ns. Значит зависимости между `ai` и `database` (Dragonfly CR → dragonfly-operator) будут писаться с явным `dependsOn[].namespace`. Комментарий теперь это и описывает.Reviewed-on: #12

Co-authored-by: Hermes Agent <hermes@grachevko.ru>
2026-09-17 21:18:22 +00:00
21 changed files with 354 additions and 5 deletions
+12
View File
@@ -0,0 +1,12 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ai
components:
- ../../components/sops
resources:
- ./namespace.yaml
- ./mcp-k8s/ks.yaml
- ./toolhive/ks.yaml
@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./serviceaccount.yaml
- ./rbac.yaml
- ./mcpserver.yaml
@@ -0,0 +1,38 @@
---
# Kubernetes MCP Server: containers/kubernetes-mcp-server (Apache-2.0, Go, без обёрток над kubectl).
# Транспорт streamable-http — сервер сам отдаёт MCP по HTTP на --port 8080, путь /mcp,
# ровно то, что ожидает прокси ToolHive (обёртка над stdio не нужна).
#
# --read-only — штатный режим сервера: ни одной write-операции (create/update/delete).
# Это второй барьер после RBAC SA (см. rbac.yaml); третий — denied_resources в TOML,
# если понадобится спрятать ресурс от инструментов совсем.
#
# --toolsets core оставляет базовый набор: без helm (чарты ставит Flux, агенту там
# делать нечего), tekton, kubevirt, kiali и netobserv.
#
# ВАЖНО: без oidcConfigRef прокси ToolHive работает без аутентификации — любой, кто
# достучится до порта, получит доступ к API под правами SA ниже. Поэтому наружу сервер
# не выводим: внешний доступ появится вместе с OIDC после Authelia.
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: mcp-k8s
spec:
image: quay.io/containers/kubernetes_mcp_server:v0.0.66@sha256:6d650f4bd6ac303ad82713c997e73a2d001602f9bf17392c9b9a0e30e29c6423
transport: streamable-http
args:
- --port
- "8080"
- --read-only
- --toolsets
- core
serviceAccount: mcp-k8s
permissionProfile:
type: builtin
name: network
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
+47
View File
@@ -0,0 +1,47 @@
---
# Базовое чтение даём встроенной ролью view — её же рекомендует официальный гайд
# containers/kubernetes-mcp-server (docs/getting-started-kubernetes.md). view
# описывает namespaced-ресурсы и secrets в неё не входят, поэтому именно она, а не
# самодельный список с resources: ["*"].
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: mcp-k8s-view
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
name: mcp-k8s
namespace: ai
---
# view включает только namespaced-ресурсы, поэтому кластерные объекты и метрики
# добавляем отдельно. Ни secrets, ни записи здесь нет.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: mcp-k8s-cluster-read
rules:
- apiGroups: [""]
resources: ["nodes", "namespaces", "persistentvolumes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses", "volumeattachments", "csidrivers", "csistoragecapacities"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["*"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: mcp-k8s-cluster-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: mcp-k8s-cluster-read
subjects:
- kind: ServiceAccount
name: mcp-k8s
namespace: ai
@@ -0,0 +1,8 @@
---
# SA создаётся здесь намеренно: spec.serviceAccount в MCPServer означает «уже
# существующий» — если его не создать, оператор сделает свой и права выдавать
# будет некуда.
apiVersion: v1
kind: ServiceAccount
metadata:
name: mcp-k8s
+19
View File
@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: mcp-k8s
spec:
interval: 1h
path: ./kubernetes/apps/ai/mcp-k8s/app
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: ai
wait: true
# Зависимость внутри своего ns, поэтому поле namespace не нужно (по CRD оно
# по умолчанию равно ns самого объекта-зависимости).
dependsOn:
- name: toolhive-operator
+7
View File
@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: ai
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
@@ -0,0 +1,19 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: toolhive-operator
spec:
interval: 1h
chartRef:
kind: OCIRepository
name: toolhive-operator
values:
operator:
replicaCount: 1
resources:
requests:
cpu: 10m
memory: 128Mi
limits:
memory: 512Mi
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./ocirepository.yaml
- ./helmrelease.yaml
@@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: toolhive-operator
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
# Версия чарта toolhive-operator; теги чартов у stacklok идут без префикса v.
tag: 0.49.0
url: oci://ghcr.io/stacklok/toolhive/toolhive-operator
@@ -0,0 +1,10 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: toolhive-operator-crds
spec:
interval: 1h
chartRef:
kind: OCIRepository
name: toolhive-operator-crds
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./ocirepository.yaml
- ./helmrelease.yaml
@@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: toolhive-operator-crds
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
# Версия чарта toolhive-operator-crds; теги чартов у stacklok идут без префикса v.
tag: 0.49.0
url: oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds
+44
View File
@@ -0,0 +1,44 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: toolhive-operator-crds
spec:
interval: 1h
path: ./kubernetes/apps/ai/toolhive/crds
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: ai
wait: true
healthChecks:
- apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: toolhive-operator-crds
namespace: ai
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: toolhive-operator
spec:
interval: 1h
path: ./kubernetes/apps/ai/toolhive/app
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: ai
wait: true
healthChecks:
- apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: toolhive-operator
namespace: ai
# CRD-чарт ставится отдельной Kustomization: CRD должны существовать до
# установки оператора (иначе MCPServer/ToolHive-объекты не имеют схемы).
dependsOn:
- name: toolhive-operator-crds
@@ -0,0 +1,38 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: dragonfly-operator
spec:
interval: 1h
chartRef:
kind: OCIRepository
name: dragonfly-operator
values:
# CRD (dragonflydb.io) идут обычным шаблоном чарта, а не каталогом crds/,
# поэтому обновляются вместе с релизом; keep: true не даёт Helm удалить их
# при удалении релиза (иначе вместе с CRD уехали бы все Dragonfly CR).
crds:
install: true
keep: true
# Метрики оператора отдаются напрямую на :8080, а не через kube-rbac-proxy.
# Prometheus Operator в кластере нет, поэтому ServiceMonitor и дашборд Grafana
# не включаем: без их CRD релиз бы не встал.
rbacProxy:
enabled: false
serviceMonitor:
enabled: false
grafanaDashboard:
enabled: false
manager:
# Дефолт чарта — docker.dragonflydb.io/dragonflydb/operator; берём тот же
# образ из ghcr и фиксируем дайджестом.
image:
repository: ghcr.io/dragonflydb/operator
tag: v1.6.1@sha256:b11411142935f92ed0ec30a5ddeb31680e09ab66beecb827cb0224f1c4238638
resources:
requests:
cpu: 10m
memory: 128Mi
limits:
memory: 256Mi
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./ocirepository.yaml
- ./helmrelease.yaml
@@ -0,0 +1,15 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: dragonfly-operator
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
# Версия чарта dragonfly-operator. В самом Chart.yaml version/appVersion
# записаны как v1.6.1, поэтому тег идёт с префиксом v.
tag: v1.6.1
url: oci://ghcr.io/dragonflydb/dragonfly-operator/helm/dragonfly-operator
@@ -0,0 +1,24 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: dragonfly-operator
spec:
interval: 1h
path: ./kubernetes/apps/database/dragonfly-operator/app
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: database
# wait + healthCheck: Kustomization считается Ready только когда оператор
# действительно поднялся. На неё будут ссылаться через dependsOn те
# Kustomization, которые создают Dragonfly CR (ns потребителя),
# поэтому без этого dependsOn не гейтил бы ничего.
wait: true
healthChecks:
- apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: dragonfly-operator
namespace: database
@@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: database
components:
- ../../components/sops
resources:
- ./namespace.yaml
- ./dragonfly-operator/ks.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: database
@@ -19,10 +19,9 @@ spec:
name: external-secrets name: external-secrets
namespace: security namespace: security
# Оператор ходит в Vault через kubernetes-auth, поэтому ждём готовности Vault. # Оператор ходит в Vault через kubernetes-auth, поэтому ждём готовности Vault.
# dependsOn адресует Flux-Kustomization, а не приложение: поле namespace необязательно # dependsOn адресует Flux-Kustomization в её собственном ns: поле namespace
# и по умолчанию равно ns САМОГО объекта-зависимости (CRD kustomize.toolkit.fluxcd.io), # необязательно и по умолчанию равно ns самого объекта-зависимости
# а не ns, куда тот раскладывает ресурсы. Пример: ewatkins/apps/default/netbox зависит от # (CRD kustomize.toolkit.fluxcd.io), а не ns, куда тот раскладывает ресурсы.
# dragonfly и crunchy-postgres-*, и все три объекта объявлены в flux-system, при том что # Обе Kustomization лежат в ns security, поэтому поле не нужно.
# ресурсы уезжают в default/database.
dependsOn: dependsOn:
- name: vault - name: vault