added repo
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: kubectl-mcp-server
|
||||
description: >-
|
||||
Read-only Kubernetes RCA MCP server (single-cluster + multi-cluster
|
||||
federation). Env-driven; exposes only an allowlisted diagnostic toolset
|
||||
over streamable-http for AI clients (Bifrost). Diagnosis only — never
|
||||
mutates cluster state.
|
||||
version: 2.1.0
|
||||
appVersion: "0.1.0"
|
||||
sources:
|
||||
- https://github.com/Meesho/kubernetes-mcp
|
||||
@@ -0,0 +1,178 @@
|
||||
# kubectl-mcp-server
|
||||
|
||||
Read-only Kubernetes RCA (root cause analysis) MCP server. A diagnosis-only
|
||||
tool surface for an RCA bot / AI client (Bifrost) — it can **never** mutate
|
||||
cluster state. The image entrypoint is `k8s-rca-mcp`, configured **entirely
|
||||
via env vars** (no CLI args), and exposes only an allowlisted read-only
|
||||
toolset over streamable-http.
|
||||
|
||||
> The chart/release keeps the name `kubectl-mcp-server` for continuity with
|
||||
> the existing ArgoCD apps and overrides. The underlying server is the
|
||||
> in-house env-driven RCA build (`prd/.../kubectl-mcp-server:v2`).
|
||||
|
||||
## Two modes (`mcp.mode`)
|
||||
|
||||
| | `single` | `multi` (hub) |
|
||||
|---|---|---|
|
||||
| Scope | one cluster, in-cluster SA | federation router |
|
||||
| `cluster` tool param | none | **required** on every tool |
|
||||
| Deploy where | every cluster | one hub |
|
||||
| Credentials | own ServiceAccount | none for remotes — forwards MCP calls with a per-backend bearer token |
|
||||
|
||||
In `multi` mode every tool gains a required `cluster` argument. `cluster == self`
|
||||
is served natively via the hub's in-cluster SA; any other registered name is
|
||||
resolved to a DNS endpoint and the identical tool call is forwarded over
|
||||
streamable-http to that cluster's `single`-mode instance. The hub holds **no
|
||||
remote kubeconfigs** — only endpoints and bearer tokens.
|
||||
|
||||
## Authentication
|
||||
|
||||
Inbound: the MCP endpoint requires a bearer token. On http transport the
|
||||
server **refuses to start** unless `MCP_AUTH_TOKEN` is present (or
|
||||
`auth.allowAnonymous: true`). Clients send `Authorization: Bearer <token>`.
|
||||
|
||||
All bearer tokens (the inbound `MCP_AUTH_TOKEN` and, on a hub, every backend
|
||||
token) are stored in **Vault** and synced via an `ExternalSecret` — never in
|
||||
values or git. One ExternalSecret extracts a Vault path into the Secret
|
||||
`kubectl-mcp-server-creds`, which the deployment injects with `envFrom`, so
|
||||
each Vault key becomes a container env var verbatim.
|
||||
|
||||
```yaml
|
||||
externalSecrets:
|
||||
enabled: true
|
||||
refreshInterval: "150s"
|
||||
secretStoreRef:
|
||||
name: vault-backend
|
||||
kind: ClusterSecretStore
|
||||
dataFrom:
|
||||
secretKey: "meesho/stg/cntr/devop/kubectl-mcp-server" # Vault path (configurable)
|
||||
```
|
||||
|
||||
Store these **keys** at that Vault path (key names map 1:1 to env vars, so
|
||||
they must be valid env var names):
|
||||
|
||||
| Vault key | Meaning | When |
|
||||
|---|---|---|
|
||||
| `MCP_AUTH_TOKEN` | inbound bearer the RCA agent must present | always |
|
||||
| `<TOKENENV>` per backend, e.g. `SUPPLY_MCP_TOKEN` | bearer the hub uses to call that backend | multi mode |
|
||||
|
||||
## Single-mode deploy
|
||||
|
||||
Per cluster, set the Vault path and (optionally) the ingress host. Example
|
||||
override (`helm-overrides/<cluster>/kubectl-mcp-server/custom-values.yaml`):
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
mode: single
|
||||
transport: http
|
||||
host: "0.0.0.0"
|
||||
port: 8000
|
||||
|
||||
auth:
|
||||
allowAnonymous: false
|
||||
|
||||
externalSecrets:
|
||||
enabled: true
|
||||
dataFrom:
|
||||
secretKey: "meesho/stg/cntr/devop/kubectl-mcp-server" # must hold MCP_AUTH_TOKEN
|
||||
```
|
||||
|
||||
Seed the Vault path:
|
||||
|
||||
```bash
|
||||
vault kv put meesho/stg/cntr/devop/kubectl-mcp-server \
|
||||
MCP_AUTH_TOKEN=$(openssl rand -hex 32)
|
||||
```
|
||||
|
||||
## Multi-mode (hub): adding a cluster + its token
|
||||
|
||||
Adding a backend is exactly two coordinated steps — a **registry entry** that
|
||||
names an env var, and a **Vault key of that same name** holding the value.
|
||||
|
||||
### 1. Add the cluster to the registry (`federation.clusters`)
|
||||
|
||||
In the hub's override:
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
mode: multi
|
||||
|
||||
externalSecrets:
|
||||
enabled: true
|
||||
dataFrom:
|
||||
secretKey: "meesho/stg/cntr/devop/kubectl-mcp-server-hub"
|
||||
|
||||
federation:
|
||||
forwardTimeout: 30
|
||||
clusters:
|
||||
- name: self # the hub's own cluster, served natively
|
||||
self: true
|
||||
- name: supply # the name the RCA bot passes as cluster=
|
||||
endpoint: https://kubectl-mcp-server.supply.stg.meesho.int/mcp # must end in /mcp
|
||||
tokenEnv: SUPPLY_MCP_TOKEN # env var the hub reads this backend's bearer from
|
||||
- name: payments
|
||||
endpoint: https://kubectl-mcp-server.payments.stg.meesho.int/mcp
|
||||
tokenEnv: PAYMENTS_MCP_TOKEN
|
||||
```
|
||||
|
||||
`tokenEnv` is a name you choose (convention: `<CLUSTER>_MCP_TOKEN`). It is the
|
||||
env var the hub looks up when forwarding to that cluster.
|
||||
|
||||
### 2. Add the matching token to Vault
|
||||
|
||||
Add the backend tokens as keys at the hub's Vault path, named **exactly** like
|
||||
each `tokenEnv`:
|
||||
|
||||
```bash
|
||||
vault kv put meesho/stg/cntr/devop/kubectl-mcp-server-hub \
|
||||
MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
|
||||
SUPPLY_MCP_TOKEN=<the supply cluster's own MCP_AUTH_TOKEN> \
|
||||
PAYMENTS_MCP_TOKEN=<the payments cluster's own MCP_AUTH_TOKEN>
|
||||
```
|
||||
|
||||
> **Critical:** the value of `SUPPLY_MCP_TOKEN` must equal the
|
||||
> `MCP_AUTH_TOKEN` that the **supply cluster's own** single-mode deployment
|
||||
> uses (from *its* Vault path). That is how the hub authenticates to it.
|
||||
|
||||
Nothing else changes — `envFrom: secretRef` injects every Vault key
|
||||
automatically, so the new `*_MCP_TOKEN` env var just appears in the container.
|
||||
**Adding a cluster = one registry entry + one Vault key of the same name.**
|
||||
|
||||
### Request flow for one backend
|
||||
|
||||
```
|
||||
RCA bot calls a tool with cluster="supply"
|
||||
│
|
||||
▼
|
||||
hub registry: supply → endpoint + tokenEnv=SUPPLY_MCP_TOKEN
|
||||
│ └── value comes from Vault key SUPPLY_MCP_TOKEN (via envFrom)
|
||||
▼
|
||||
hub POSTs to https://...supply.../mcp with Authorization: Bearer <SUPPLY_MCP_TOKEN>
|
||||
▼
|
||||
supply single-mode pod checks it == its own MCP_AUTH_TOKEN ✓ → serves the tool
|
||||
result returned tagged with cluster="supply"
|
||||
```
|
||||
|
||||
Structured errors (never silent): missing/empty `cluster` → `missing_cluster`;
|
||||
unknown name → `unknown_cluster` (both list available clusters); backend down
|
||||
or slow → `backend_unreachable` / `backend_timeout`, tagged with the cluster.
|
||||
|
||||
## Key values
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `mcp.mode` | `single` | `single` or `multi` |
|
||||
| `mcp.transport` | `http` | streamable-http |
|
||||
| `mcp.port` | `8000` | matches probes + service |
|
||||
| `auth.allowAnonymous` | `false` | skip the bearer (testing only) |
|
||||
| `externalSecrets.enabled` | `true` | sync tokens from Vault |
|
||||
| `externalSecrets.dataFrom.secretKey` | `""` | **Vault path (set per cluster)** |
|
||||
| `externalSecrets.secretStoreRef.name` | `vault-backend` | ClusterSecretStore |
|
||||
| `externalSecrets.refreshInterval` | `150s` | |
|
||||
| `envFrom` | `[]` | extra envFrom sources (non-Vault testing) |
|
||||
| `federation.forwardTimeout` | `30` | seconds per forwarded call (multi) |
|
||||
| `federation.clusters` | `[{name: self, self: true}]` | registry (multi) |
|
||||
|
||||
RBAC: a read-only ClusterRole (`get/list/watch` only, `secrets` excluded) is
|
||||
created for the pod ServiceAccount — the enforcement layer behind the server's
|
||||
own deny-by-default tool allowlist.
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- define "kubectl-mcp-server.name" -}}
|
||||
{{- .Values.fullnameOverride | default .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "kubectl-mcp-server.labels" -}}
|
||||
app: {{ include "kubectl-mcp-server.name" . }}
|
||||
{{- with .Values.labels }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "kubectl-mcp-server.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.name }}{{ .Values.serviceAccount.name }}{{ else }}{{ include "kubectl-mcp-server.name" . }}{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Secret synced from Vault by the ExternalSecret; holds all bearer tokens. */ -}}
|
||||
{{- define "kubectl-mcp-server.credsSecretName" -}}
|
||||
{{- .Values.externalSecrets.secretName | default (printf "%s-creds" (include "kubectl-mcp-server.name" .)) }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,160 @@
|
||||
{{- if .Values.rbac.create -}}
|
||||
# Read-only ClusterRole for kubectl-mcp-server.
|
||||
#
|
||||
# * get/list/watch ONLY — no create/update/patch/delete anywhere. This is
|
||||
# the enforcement layer behind the server's own deny-by-default allowlist
|
||||
# (the server never registers any write/destructive tool).
|
||||
# * `secrets` is EXCLUDED from core resources — the RCA bot has no business
|
||||
# reading Secret payloads, and the server omits get_secrets entirely.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.name" . }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
rules:
|
||||
# Core API resources - read access (secrets intentionally omitted)
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
- pods/log
|
||||
- pods/status
|
||||
- services
|
||||
- endpoints
|
||||
- persistentvolumeclaims
|
||||
- persistentvolumes
|
||||
- configmaps
|
||||
- serviceaccounts
|
||||
- namespaces
|
||||
- nodes
|
||||
- nodes/status
|
||||
- events
|
||||
- resourcequotas
|
||||
- limitranges
|
||||
- replicationcontrollers
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Apps API resources
|
||||
- apiGroups: ["apps"]
|
||||
resources:
|
||||
- deployments
|
||||
- daemonsets
|
||||
- replicasets
|
||||
- statefulsets
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Batch API resources
|
||||
- apiGroups: ["batch"]
|
||||
resources:
|
||||
- jobs
|
||||
- cronjobs
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Networking API resources
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources:
|
||||
- ingresses
|
||||
- networkpolicies
|
||||
- ingressclasses
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Storage API resources
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources:
|
||||
- storageclasses
|
||||
- volumeattachments
|
||||
- csidrivers
|
||||
- csinodes
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# RBAC API resources
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources:
|
||||
- roles
|
||||
- rolebindings
|
||||
- clusterroles
|
||||
- clusterrolebindings
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Autoscaling API resources
|
||||
- apiGroups: ["autoscaling"]
|
||||
resources:
|
||||
- horizontalpodautoscalers
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Policy API resources
|
||||
- apiGroups: ["policy"]
|
||||
resources:
|
||||
- poddisruptionbudgets
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# API extensions
|
||||
- apiGroups: ["apiextensions.k8s.io"]
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Admissionregistration API
|
||||
- apiGroups: ["admissionregistration.k8s.io"]
|
||||
resources:
|
||||
- mutatingwebhookconfigurations
|
||||
- validatingwebhookconfigurations
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Certificates API
|
||||
- apiGroups: ["certificates.k8s.io"]
|
||||
resources:
|
||||
- certificatesigningrequests
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Coordination API
|
||||
- apiGroups: ["coordination.k8s.io"]
|
||||
resources:
|
||||
- leases
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Metrics API for resource usage (kubectl top)
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources:
|
||||
- nodes
|
||||
- pods
|
||||
verbs: ["get", "list"]
|
||||
|
||||
# Discovery API
|
||||
- apiGroups: ["discovery.k8s.io"]
|
||||
resources:
|
||||
- endpointslices
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# KEDA + Argo Rollouts CRs (read-only) — common at Meesho, used by the
|
||||
# server's keda_* / rollouts_* inspection tools
|
||||
- apiGroups: ["keda.sh"]
|
||||
resources:
|
||||
- scaledobjects
|
||||
- scaledjobs
|
||||
- triggerauthentications
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["argoproj.io"]
|
||||
resources:
|
||||
- rollouts
|
||||
- analysisruns
|
||||
- analysistemplates
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Contour CRs (read-only) — Meesho ingress is Contour HTTPProxy almost
|
||||
# everywhere; needed to answer "what's the route/hostname for service X"
|
||||
- apiGroups: ["projectcontour.io"]
|
||||
resources:
|
||||
- httpproxies
|
||||
- tlscertificatedelegations
|
||||
- extensionservices
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Flagger canaries (read-only) — canary.enabled is standard in the 2.0.0
|
||||
# chart; lets the server explain canary rollout state
|
||||
- apiGroups: ["flagger.app"]
|
||||
resources:
|
||||
- canaries
|
||||
- metrictemplates
|
||||
verbs: ["get", "list", "watch"]
|
||||
{{- end }}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{- if .Values.rbac.create -}}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.name" . }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "kubectl-mcp-server.name" . }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "kubectl-mcp-server.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- /*
|
||||
Federation registry (multi mode only). Mounted at /etc/k8s-rca-mcp and read
|
||||
by the hub at startup. The hub stores no kubeconfigs — only endpoints and the
|
||||
names of the env vars holding each backend's bearer token.
|
||||
*/ -}}
|
||||
{{- if eq .Values.mcp.mode "multi" -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.name" . }}-registry
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
data:
|
||||
clusters.yaml: |
|
||||
clusters:
|
||||
{{- toYaml .Values.federation.clusters | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,112 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "kubectl-mcp-server.name" . }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "kubectl-mcp-server.name" . }}
|
||||
{{- with .Values.labels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- if eq .Values.mcp.mode "multi" }}
|
||||
checksum/registry: {{ toYaml .Values.federation.clusters | sha256sum }}
|
||||
{{- end }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ include "kubectl-mcp-server.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
{{- with .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: {{ .Values.priorityClassName }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: kubectl-mcp-server
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# Image ENTRYPOINT is `k8s-rca-mcp`, configured ENTIRELY via env vars
|
||||
# (no CLI args). The server registers only an allowlisted read-only
|
||||
# toolset — writes are blocked at the app layer and again by RBAC.
|
||||
env:
|
||||
- name: MCP_MODE
|
||||
value: {{ .Values.mcp.mode | quote }}
|
||||
- name: MCP_TRANSPORT
|
||||
value: {{ .Values.mcp.transport | quote }}
|
||||
- name: MCP_HOST
|
||||
value: {{ .Values.mcp.host | quote }}
|
||||
- name: MCP_PORT
|
||||
value: {{ .Values.mcp.port | quote }}
|
||||
{{- if .Values.auth.allowAnonymous }}
|
||||
- name: MCP_ALLOW_ANONYMOUS
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if eq .Values.mcp.mode "multi" }}
|
||||
- name: MCP_CLUSTER_REGISTRY
|
||||
value: /etc/k8s-rca-mcp/clusters.yaml
|
||||
- name: MCP_FORWARD_TIMEOUT
|
||||
value: {{ .Values.federation.forwardTimeout | quote }}
|
||||
{{- end }}
|
||||
# Bearer tokens come from Vault via the ExternalSecret-synced Secret:
|
||||
# MCP_AUTH_TOKEN (inbound) + each backend's <TOKENENV> (multi mode).
|
||||
{{- if or .Values.externalSecrets.enabled .Values.envFrom }}
|
||||
envFrom:
|
||||
{{- if .Values.externalSecrets.enabled }}
|
||||
- secretRef:
|
||||
name: {{ include "kubectl-mcp-server.credsSecretName" . }}
|
||||
{{- end }}
|
||||
{{- with .Values.envFrom }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.mcp.port }}
|
||||
{{- with .Values.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- if eq .Values.mcp.mode "multi" }}
|
||||
volumeMounts:
|
||||
- name: registry
|
||||
mountPath: /etc/k8s-rca-mcp
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if eq .Values.mcp.mode "multi" }}
|
||||
volumes:
|
||||
- name: registry
|
||||
configMap:
|
||||
name: {{ include "kubectl-mcp-server.name" . }}-registry
|
||||
{{- end }}
|
||||
@@ -0,0 +1,38 @@
|
||||
{{- /*
|
||||
ExternalSecret (external-secrets.io) — pulls every bearer token from one Vault
|
||||
path and syncs them into the K8s Secret `<name>-creds`. The deployment injects
|
||||
that Secret with `envFrom`, so each Vault key becomes an env var verbatim.
|
||||
|
||||
Store these keys at the Vault path (externalSecrets.dataFrom.secretKey):
|
||||
* MCP_AUTH_TOKEN — inbound bearer the RCA agent must present
|
||||
* <TOKENENV> per backend — e.g. SUPPLY_MCP_TOKEN, referenced by the
|
||||
registry's `tokenEnv:` (multi mode only)
|
||||
Keys MUST be valid env var names (they map 1:1 to container env).
|
||||
*/ -}}
|
||||
{{- if .Values.externalSecrets.enabled -}}
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.credsSecretName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-5"
|
||||
spec:
|
||||
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
|
||||
secretStoreRef:
|
||||
name: {{ .Values.externalSecrets.secretStoreRef.name }}
|
||||
kind: {{ .Values.externalSecrets.secretStoreRef.kind }}
|
||||
target:
|
||||
name: {{ include "kubectl-mcp-server.credsSecretName" . }}
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Retain
|
||||
dataFrom:
|
||||
- extract:
|
||||
conversionStrategy: Default
|
||||
key: {{ required "externalSecrets.dataFrom.secretKey (Vault path) is required when externalSecrets.enabled" .Values.externalSecrets.dataFrom.secretKey }}
|
||||
{{- if .Values.externalSecrets.dataFrom.version }}
|
||||
version: {{ .Values.externalSecrets.dataFrom.version | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,29 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- if .Values.createContourGateway -}}
|
||||
{{- if or ( eq "contour-internal" .Values.ingress.ingressClassName ) ( eq "contour-external" .Values.ingress.ingressClassName ) ( eq "contour-internal-0" .Values.ingress.ingressClassName ) ( eq "contour-internal-1" .Values.ingress.ingressClassName ) ( eq "contour-external-0" .Values.ingress.ingressClassName ) ( eq "contour-external-1" .Values.ingress.ingressClassName ) }}
|
||||
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
|
||||
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
|
||||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
name: {{ $serviceName }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
projectcontour.io/ingress.class: {{ .Values.ingress.ingressClassName }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.ingressClassName }}
|
||||
routes:
|
||||
- services:
|
||||
- name: {{ $serviceName }}
|
||||
port: {{ $servicePortNumber }}
|
||||
{{- if .Values.ingress.slowStart.enabled }}
|
||||
slowStartPolicy:
|
||||
window: {{ .Values.ingress.slowStart.window }}
|
||||
aggression: {{ .Values.ingress.slowStart.aggression | float64 | squote }}
|
||||
minWeightPercent: {{ .Values.ingress.slowStart.minPercent }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,39 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- if .Values.createContourGateway -}}
|
||||
{{- if or (eq "contour-internal" .Values.ingress.ingressClassName) (eq "contour-internal-0" .Values.ingress.ingressClassName) (eq "contour-internal-1" .Values.ingress.ingressClassName) }}
|
||||
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
|
||||
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
|
||||
|
||||
{{- $intraIngressClass := "" -}}
|
||||
{{- if eq .Values.ingress.ingressClassName "contour-internal" -}}
|
||||
{{- $intraIngressClass = "contour-internal-intra" -}}
|
||||
{{- else if eq .Values.ingress.ingressClassName "contour-internal-0" -}}
|
||||
{{- $intraIngressClass = "contour-internal-intra-0" -}}
|
||||
{{- else if eq .Values.ingress.ingressClassName "contour-internal-1" -}}
|
||||
{{- $intraIngressClass = "contour-internal-intra-1" -}}
|
||||
{{- end -}}
|
||||
|
||||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
name: {{ $serviceName }}-intra
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
projectcontour.io/ingress.class: {{ $intraIngressClass }}
|
||||
spec:
|
||||
ingressClassName: {{ $intraIngressClass }}
|
||||
routes:
|
||||
- services:
|
||||
- name: {{ $serviceName }}
|
||||
port: {{ $servicePortNumber }}
|
||||
{{- if .Values.ingress.slowStart.enabled }}
|
||||
slowStartPolicy:
|
||||
window: {{ .Values.ingress.slowStart.window }}
|
||||
aggression: {{ .Values.ingress.slowStart.aggression | float64 | squote }}
|
||||
minWeightPercent: {{ .Values.ingress.slowStart.minPercent }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- if .Values.createContourGateway -}}
|
||||
{{- if or (eq "contour-internal" .Values.ingress.ingressClassName) (eq "contour-internal-0" .Values.ingress.ingressClassName) (eq "contour-internal-1" .Values.ingress.ingressClassName) }}
|
||||
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
|
||||
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
|
||||
|
||||
{{- $intraIngressClass := "" -}}
|
||||
{{- if eq .Values.ingress.ingressClassName "contour-internal" -}}
|
||||
{{- $intraIngressClass = "contour-internal-intra" -}}
|
||||
{{- else if eq .Values.ingress.ingressClassName "contour-internal-0" -}}
|
||||
{{- $intraIngressClass = "contour-internal-intra-0" -}}
|
||||
{{- else if eq .Values.ingress.ingressClassName "contour-internal-1" -}}
|
||||
{{- $intraIngressClass = "contour-internal-intra-1" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{ $count := 0 | int }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
name: {{ $serviceName }}-intra-{{ $count }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" $ | nindent 4 }}
|
||||
annotations:
|
||||
projectcontour.io/ingress.class: {{ $intraIngressClass }}
|
||||
spec:
|
||||
ingressClassName: {{ $intraIngressClass }}
|
||||
virtualhost:
|
||||
fqdn: "{{ .host }}"
|
||||
includes:
|
||||
{{- range .paths }}
|
||||
- conditions:
|
||||
{{- if or (eq (lower .pathType) "prefix") (eq (lower .pathType) "implementationspecific") }}
|
||||
- prefix: {{ .path }}
|
||||
{{- end }}
|
||||
{{- if eq (lower .pathType) "exact" }}
|
||||
- exact: {{ .path }}
|
||||
{{- end }}
|
||||
name: {{ $serviceName }}-intra
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ $count = add1 $count }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,39 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- if .Values.createContourGateway -}}
|
||||
{{- if or ( eq "contour-internal" .Values.ingress.ingressClassName ) ( eq "contour-external" .Values.ingress.ingressClassName ) ( eq "contour-internal-0" .Values.ingress.ingressClassName ) ( eq "contour-internal-1" .Values.ingress.ingressClassName ) ( eq "contour-external-0" .Values.ingress.ingressClassName ) ( eq "contour-external-1" .Values.ingress.ingressClassName ) }}
|
||||
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
|
||||
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
|
||||
{{- $ingressClassName := .Values.ingress.ingressClassName -}}
|
||||
{{ $count := 0 | int }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
name: {{ $serviceName }}-{{ $count }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" $ | nindent 4 }}
|
||||
annotations:
|
||||
projectcontour.io/ingress.class: {{ $ingressClassName }}
|
||||
spec:
|
||||
ingressClassName: {{ $ingressClassName }}
|
||||
virtualhost:
|
||||
fqdn: "{{ .host }}"
|
||||
includes:
|
||||
{{- range .paths }}
|
||||
- conditions:
|
||||
{{- if or ( eq ( lower .pathType ) "prefix" ) ( eq ( lower .pathType ) "implementationspecific") }}
|
||||
- prefix: {{ .path }}
|
||||
{{- end }}
|
||||
{{- if ( eq ( lower .pathType ) "exact" ) }}
|
||||
- exact: {{ .path }}
|
||||
{{- end }}
|
||||
name: {{ $serviceName }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ $count = add1 $count }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,36 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- if or ( eq "nginx-internal" .Values.ingress.ingressClassName ) ( eq "nginx-external" .Values.ingress.ingressClassName ) -}}
|
||||
{{- $serviceName := include "kubectl-mcp-server.name" . -}}
|
||||
{{- $servicePortNumber := .Values.ingress.servicePortNumber | default 8000 -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $serviceName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
{{- if .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.ingressClassName }}
|
||||
ingressClassName: {{ .Values.ingress.ingressClassName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- pathType: {{ .pathType }}
|
||||
path: {{ .path }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ $serviceName }}
|
||||
port:
|
||||
number: {{ $servicePortNumber }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.port }}
|
||||
selector:
|
||||
app: {{ include "kubectl-mcp-server.name" . }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "kubectl-mcp-server.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubectl-mcp-server.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
# Unlike elasticsearch-mcp (which authenticates to ES via ExternalSecret
|
||||
# creds), kubectl-mcp-server authenticates to the API server with this SA's
|
||||
# token via in-cluster config — the token MUST be mounted.
|
||||
automountServiceAccountToken: true
|
||||
{{- end }}
|
||||
@@ -0,0 +1,162 @@
|
||||
fullnameOverride: "kubectl-mcp-server"
|
||||
|
||||
replicas: 1
|
||||
|
||||
image:
|
||||
repository: asia-southeast1-docker.pkg.dev/meesho-devops-admin-0622/prd/devop/kubectl-mcp-server
|
||||
tag: v2
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
labels:
|
||||
bu: infra
|
||||
team: devops
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
# RBAC — ClusterRole + ClusterRoleBinding for the pod ServiceAccount.
|
||||
# The role grants get/list/watch ONLY (no create/update/patch/delete) and
|
||||
# deliberately excludes `secrets`. This is the enforcement layer behind the
|
||||
# server's own deny-by-default allowlist (no write/secret tools registered).
|
||||
rbac:
|
||||
create: true
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
priorityClassName: ""
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# --- MCP server settings ---------------------------------------------------
|
||||
# The image ENTRYPOINT is `k8s-rca-mcp`, configured ENTIRELY via env vars
|
||||
# (there are NO CLI args). transport: http is streamable-http for Bifrost /
|
||||
# HTTP MCP clients.
|
||||
mcp:
|
||||
mode: single # single | multi (federation hub)
|
||||
transport: http
|
||||
host: "0.0.0.0"
|
||||
port: 8000
|
||||
|
||||
# --- inbound auth on the MCP endpoint --------------------------------------
|
||||
# Bearer token. On http transport the server REFUSES TO START unless the
|
||||
# MCP_AUTH_TOKEN env var is present OR allowAnonymous is true. The token is
|
||||
# NOT stored in values/git — it is pulled from Vault (see externalSecrets).
|
||||
# Clients send: Authorization: Bearer <token>
|
||||
auth:
|
||||
allowAnonymous: false
|
||||
|
||||
# --- secrets from Vault (external-secrets.io) ------------------------------
|
||||
# One ExternalSecret extracts a Vault path into the Secret `<name>-creds`,
|
||||
# which the Deployment injects with `envFrom`. Every key at the Vault path
|
||||
# becomes a container env var verbatim, so store them as env var NAMES:
|
||||
# MCP_AUTH_TOKEN — inbound bearer (always)
|
||||
# <TOKENENV> per backend — e.g. SUPPLY_MCP_TOKEN (multi mode only),
|
||||
# matching the registry's `tokenEnv:` below
|
||||
externalSecrets:
|
||||
enabled: true
|
||||
refreshInterval: "150s"
|
||||
secretStoreRef:
|
||||
name: vault-backend
|
||||
kind: ClusterSecretStore
|
||||
dataFrom:
|
||||
# Vault path holding the tokens — set per cluster in the override.
|
||||
secretKey: ""
|
||||
# Optional: pin a specific Vault secret version.
|
||||
version: ""
|
||||
# Override the synced Secret name (default: <name>-creds).
|
||||
secretName: ""
|
||||
|
||||
# Extra envFrom sources (merged after the Vault creds secret), e.g. a
|
||||
# manually-created Secret for local testing without Vault.
|
||||
envFrom: []
|
||||
|
||||
# --- federation (only used when mcp.mode == multi) -------------------------
|
||||
# The hub holds NO remote kubeconfigs. It forwards each tool call to the
|
||||
# target cluster's own single-mode MCP using a per-backend bearer token.
|
||||
# Those tokens live in Vault (keys named to match each `tokenEnv:` below).
|
||||
federation:
|
||||
forwardTimeout: 30
|
||||
# Rendered into a ConfigMap mounted at /etc/k8s-rca-mcp/clusters.yaml.
|
||||
# Exactly one entry must be `self: true`.
|
||||
clusters:
|
||||
- name: self
|
||||
self: true
|
||||
# - name: supply
|
||||
# endpoint: https://kubectl-mcp-server.supply.stg.meesho.int/mcp
|
||||
# tokenEnv: SUPPLY_MCP_TOKEN # <- must be a key at the Vault path
|
||||
|
||||
# NOTE: tcpSocket, not httpGet — under streamable-http transport FastMCP
|
||||
# serves ONLY /mcp (GET /health 404s; upstream's chart gets this wrong).
|
||||
# /mcp itself returns 406 to plain GETs, so TCP is the reliable signal.
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
service:
|
||||
port: 8000
|
||||
type: ClusterIP
|
||||
|
||||
# Ingress — 2.0.0-chart style. nginx-* classes render a networking.k8s.io
|
||||
# Ingress; contour-* classes (with createContourGateway: true) render the
|
||||
# standard Meesho HTTPProxy parent/child pair plus the -intra variant.
|
||||
createContourGateway: true
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: nginx-internal
|
||||
servicePortNumber: 8000
|
||||
hosts:
|
||||
- host: ""
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx-internal
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||
nginx.ingress.kubernetes.io/limit-rps: "10"
|
||||
nginx.ingress.kubernetes.io/limit-connections: "20"
|
||||
slowStart:
|
||||
enabled: false
|
||||
window: "120s"
|
||||
aggression: 1
|
||||
minPercent: 10
|
||||
Reference in New Issue
Block a user