# 运维平台（MDIS）单文件部署清单 —— hostPath 方案
#
# 命名空间随清单一起创建，直接 kubectl apply -f ops.yaml 即可，无需先手工建。
# 要改名的话：把本文件里所有 hap-ops 一并替换（含 ConfigMap 里的 Service FQDN）。
apiVersion: v1
kind: Namespace
metadata:
  name: hap-ops
  labels:
    app.kubernetes.io/part-of: mdis
    # 不注入 istio sidecar：平台是旁路观测组件，不参与业务 mesh，
    # 注入了反而给自己的 trace 数据添噪音。
    istio-injection: disabled
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ops-config
  namespace: hap-ops
data:
  TZ: "Asia/Shanghai"
  ENV_OPS_TOKEN: "your-ops-access-token-change-me"     # 首次部署务必修改
  # 网关 nginx 动态解析不走 K8s search 域，凡经网关反代的内部地址必须写 Service 完整域名
  # （命名空间不是 hap-ops 时请同步修改）
  ENV_PROMETHEUS_SERVER: "http://ops-prometheus.hap-ops.svc.cluster.local:9090"
  ENV_PROMETHEUS_GRAFANA: "http://ops-grafana.hap-ops.svc.cluster.local:3000"
  ENV_LOKI_URL: "http://ops-loki.hap-ops.svc.cluster.local:3100"
  ENV_TEMPO_URL: "http://ops-tempo.hap-ops.svc.cluster.local:3200"
  ENV_ALLOY_URL: "http://ops-alloy.hap-ops.svc.cluster.local:12345"
  # 数据源服务发现：agent / prometheus 靠它拉取监控目标
  # 注意端口是 8081（ops-server 直接监听），不是网关对外的 48881——48881 是 nginx，没有 /api/sd 路由。
  ENV_OPS_SD_URL: "http://ops-gateway.hap-ops.svc.cluster.local:8081"
  ENV_AGENT_SD_HOST: "ops-agent.hap-ops.svc.cluster.local"
  # 被监控对象（MySQL / MongoDB / Redis / Kafka / Elasticsearch / 主机 / Flink）不在这里配置，
  # 装完在 UI「数据源」页添加或批量导入。
  # 告警子系统(1.4.0)
  ENV_OPS_MONGODB_URI: "mongodb://ops-mongo:27017"
  ENV_MONGODB_COLLECTION_PREFIX: "mdis_"
  ENV_ALERT_CRYPTO_KEY: "change-me-to-32-byte-secret"           # 务必改随机串
  # 可观测性内部地址
  ENV_TEMPO_GRPC_URL: "http://ops-tempo:4317"
  ENV_LOKI_PUSH_URL: "http://ops-loki:3100/loki/api/v1/push"
  # ---- K8s 集群监控 ----
  # 平台就跑在被监控集群里，Prometheus 用下面的 ServiceAccount 自动发现节点/Pod/ksm，
  # 装完即有 K8s 面板：不需要填任何数据源，也不需要 remote_write。
  # 不想要 K8s 面板就改成 off（能省掉 kube-state-metrics 的开销）。
  ENV_K8S_MONITOR_MODE: "incluster"
  ENV_K8S_KSM_NAMESPACE: "hap-ops"          # kube-state-metrics 所在命名空间，跟本文件保持一致
  # 集群标识：作为 origin_prometheus 标签打在本集群指标上，K8s 面板顶部按它切换集群。
  # 将来再纳管别的集群时，那些集群各起各的名字（prod-k8s / test-k8s），别重名、别用 IP。
  ENV_K8S_CLUSTER_NAME: "default"
  ENV_LOKI_RETENTION: "720h"
  ENV_TEMPO_RETENTION: "720h"
  GF_SECURITY_ALLOW_EMBEDDING: "true"
  GF_AUTH_ANONYMOUS_ENABLED: "true"
  GF_AUTH_ANONYMOUS_ORG_ROLE: "Editor"
  GF_SECURITY_COOKIE_SAMESITE: "lax"
  GF_USERS_DEFAULT_THEME: "light"
---
# 网关(对外入口)
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-gateway, namespace: hap-ops }
spec:
  replicas: 1
  selector: { matchLabels: { app: ops-gateway } }
  template:
    metadata: { labels: { app: ops-gateway } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-gateway
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: gateway }]
          envFrom: [{ configMapRef: { name: ops-config } }]
---
# 指标存储
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-prometheus, namespace: hap-ops }
spec:
  replicas: 1
  strategy: { type: Recreate }   # hostPath 数据有文件锁，禁止新旧 Pod 并存
  selector: { matchLabels: { app: ops-prometheus } }
  template:
    metadata: { labels: { app: ops-prometheus } }
    spec:
      serviceAccountName: ops-prometheus     # incluster 采集靠它自动发现集群，缺了 K8s 面板会空
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-prometheus
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: prometheus }]
          envFrom: [{ configMapRef: { name: ops-config } }]
          volumeMounts: [{ name: data, mountPath: /data/mdis/prometheus }]
      volumes:
        - name: data
          hostPath: { path: /data/mdis/prometheus, type: DirectoryOrCreate }
---
# 可视化
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-grafana, namespace: hap-ops }
spec:
  replicas: 1
  strategy: { type: Recreate }   # hostPath 数据有文件锁，禁止新旧 Pod 并存
  selector: { matchLabels: { app: ops-grafana } }
  template:
    metadata: { labels: { app: ops-grafana } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-grafana
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: grafana }]
          envFrom: [{ configMapRef: { name: ops-config } }]
          volumeMounts: [{ name: data, mountPath: /data/mdis/grafana }]
      volumes:
        - name: data
          hostPath: { path: /data/mdis/grafana, type: DirectoryOrCreate }
---
# 日志存储
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-loki, namespace: hap-ops }
spec:
  replicas: 1
  strategy: { type: Recreate }   # hostPath 数据有文件锁，禁止新旧 Pod 并存
  selector: { matchLabels: { app: ops-loki } }
  template:
    metadata: { labels: { app: ops-loki } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-loki
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: loki }]
          envFrom: [{ configMapRef: { name: ops-config } }]
          volumeMounts: [{ name: data, mountPath: /data/mdis/loki }]
      volumes:
        - name: data
          hostPath: { path: /data/mdis/loki, type: DirectoryOrCreate }
---
# 链路存储
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-tempo, namespace: hap-ops }
spec:
  replicas: 1
  strategy: { type: Recreate }   # hostPath 数据有文件锁，禁止新旧 Pod 并存
  selector: { matchLabels: { app: ops-tempo } }
  template:
    metadata: { labels: { app: ops-tempo } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-tempo
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: tempo }]
          envFrom: [{ configMapRef: { name: ops-config } }]
          volumeMounts: [{ name: data, mountPath: /data/mdis/tempo }]
      volumes:
        - name: data
          hostPath: { path: /data/mdis/tempo, type: DirectoryOrCreate }
---
# OTLP 接收(应用上报链路)。注：K8s 中 ops-alloy 仅承担 OTLP 接收，
# 日志中周期性出现 docker.sock 连接报错属预期（K8s 无 Docker 守护进程），可忽略；
# 集群容器日志采集方案见「Kubernetes 集群监控」文档。
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-alloy, namespace: hap-ops }
spec:
  replicas: 1
  selector: { matchLabels: { app: ops-alloy } }
  template:
    metadata: { labels: { app: ops-alloy } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-alloy
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: alloy }]
          envFrom: [{ configMapRef: { name: ops-config } }]
---
# 告警子系统自有存储
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-mongo, namespace: hap-ops }
spec:
  replicas: 1
  strategy: { type: Recreate }   # hostPath 数据有文件锁，禁止新旧 Pod 并存
  selector: { matchLabels: { app: ops-mongo } }
  template:
    metadata: { labels: { app: ops-mongo } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-mongo
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: mongo }]
          volumeMounts: [{ name: data, mountPath: /data/mdis/mongo }]
      volumes:
        - name: data
          hostPath: { path: /data/mdis/mongo, type: DirectoryOrCreate }
---
# 中间件 exporter(一个即可，多目标见文末补充)
apiVersion: apps/v1
kind: Deployment
metadata: { name: ops-agent, namespace: hap-ops }
spec:
  replicas: 1
  selector: { matchLabels: { app: ops-agent } }
  template:
    metadata: { labels: { app: ops-agent } }
    spec:
      nodeSelector: { hap-ops: "true" }
      containers:
        - name: ops-agent
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: agent }]
          envFrom: [{ configMapRef: { name: ops-config } }]
---
# 主机指标(每节点一个)
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: ops-nodeagent, namespace: hap-ops }
spec:
  selector: { matchLabels: { app: ops-nodeagent } }
  template:
    metadata: { labels: { app: ops-nodeagent } }
    spec:
      hostNetwork: true
      hostPID: true
      containers:
        - name: ops-nodeagent
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: nodeagent }]
          ports: [{ containerPort: 59100 }]
          volumeMounts:
            - { name: host-root, mountPath: /host, readOnly: true, mountPropagation: HostToContainer }
      volumes:
        - name: host-root
          hostPath: { path: / }
---
# ---------- Service ----------
apiVersion: v1
kind: Service
metadata: { name: ops-prometheus, namespace: hap-ops }
spec:
  selector: { app: ops-prometheus }
  ports: [{ name: server, port: 9090, targetPort: 9090 }]
---
apiVersion: v1
kind: Service
metadata: { name: ops-grafana, namespace: hap-ops }
spec:
  selector: { app: ops-grafana }
  ports: [{ name: http, port: 3000, targetPort: 3000 }]
---
apiVersion: v1
kind: Service
metadata: { name: ops-loki, namespace: hap-ops }
spec:
  selector: { app: ops-loki }
  ports: [{ name: http, port: 3100, targetPort: 3100 }]
---
apiVersion: v1
kind: Service
metadata: { name: ops-tempo, namespace: hap-ops }
spec:
  selector: { app: ops-tempo }
  ports:
    - { name: query, port: 3200, targetPort: 3200 }
    - { name: otlp-grpc, port: 4317, targetPort: 4317 }
---
apiVersion: v1
kind: Service
metadata: { name: ops-alloy, namespace: hap-ops }
spec:
  selector: { app: ops-alloy }
  ports:
    # ⚠️ appProtocol: grpc 必须保留。若被监控集群用 istio 采集链路追踪，istio 靠它
    #    把到 ops-alloy 的上游 cluster 配成 HTTP/2；缺了（端口名 otlp-grpc 前缀不是
    #    grpc-/http2-，istio 不识别）→ cluster 走 use_downstream 而非 h2 → sidecar 的
    #    OpenTelemetry tracer 建不起 gRPC 流 → 静默不产 span，全 mesh 0 trace。
    - { name: otlp-grpc, port: 4317, targetPort: 4317, appProtocol: grpc }
    - { name: otlp-http, port: 4318, targetPort: 4318 }
---
apiVersion: v1
kind: Service
metadata: { name: ops-mongo, namespace: hap-ops }
spec:
  selector: { app: ops-mongo }
  ports: [{ name: mongo, port: 27017, targetPort: 27017 }]
---
apiVersion: v1
kind: Service
metadata: { name: ops-agent, namespace: hap-ops }
spec:
  # 必须是 headless。ops-agent 为「每个数据源实例」动态起一个 exporter，端口是动态分配的
  # （mysql 19100+、es 19200+、redis 19300+、mongodb 19400+、kafka 19500+）。
  # ClusterIP 只转发 ports 里列出的端口，动态端口一律 connection refused，
  # Prometheus 会把所有中间件 target 报成 DOWN。headless 的 DNS 直接解析到 Pod IP，任意端口可直连。
  clusterIP: None
  selector: { app: ops-agent }
  ports:
    - { name: mysql, port: 9104, targetPort: 9104 }
    - { name: mongodb, port: 9216, targetPort: 9216 }
    - { name: redis, port: 9121, targetPort: 9121 }
    - { name: kafka, port: 9308, targetPort: 9308 }
    - { name: elasticsearch, port: 9114, targetPort: 9114 }
---
apiVersion: v1
kind: Service
metadata: { name: ops-gateway, namespace: hap-ops }
spec:
  type: NodePort
  selector: { app: ops-gateway }
  ports:
    # 48881 = 对外网关(nginx)；8081 = ops-server 自身，agent / prometheus 靠它取服务发现数据。
    # 只开 48881 会导致 agent 每 30 秒刷「[sd] 拉取 xxx 失败」、一个 exporter 都起不来。
    - { name: gateway, port: 48881, targetPort: 48881, nodePort: 30881 }
    - { name: server, port: 8081, targetPort: 8081 }
---
# ============================================================================
# K8s 集群监控所需的附加对象（ENV_K8S_MONITOR_MODE=incluster 时用到）
# 全部只读（get/list/watch），不含任何写操作。改成 off 的话这一整段可以不装。
# ============================================================================
# Prometheus 自动发现用的身份：读节点/服务/端点/Pod，并经 apiserver proxy 抓 kubelet 与 cadvisor
apiVersion: v1
kind: ServiceAccount
metadata: { name: ops-prometheus, namespace: hap-ops }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: { name: hap-ops-prometheus }
rules:
  - apiGroups: [""]
    resources: [nodes, nodes/proxy, nodes/metrics, services, endpoints, pods]
    verbs: [get, list, watch]
  - apiGroups: [""]
    resources: [configmaps]
    verbs: [get]
  - nonResourceURLs: ["/metrics", "/metrics/cadvisor"]
    verbs: [get]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: { name: hap-ops-prometheus }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: hap-ops-prometheus }
subjects:
  - { kind: ServiceAccount, name: ops-prometheus, namespace: hap-ops }
---
# kube-state-metrics：把集群对象状态（Deployment/Pod/PVC/Node…）转成指标
apiVersion: v1
kind: ServiceAccount
metadata: { name: kube-state-metrics, namespace: hap-ops }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: { name: mdis-kube-state-metrics }
rules:
  - apiGroups: [""]
    resources: [configmaps, secrets, nodes, pods, services, serviceaccounts, resourcequotas,
                replicationcontrollers, limitranges, persistentvolumeclaims, persistentvolumes,
                namespaces, endpoints]
    verbs: [list, watch]
  - apiGroups: ["apps"]
    resources: [statefulsets, daemonsets, deployments, replicasets]
    verbs: [list, watch]
  - apiGroups: ["batch"]
    resources: [cronjobs, jobs]
    verbs: [list, watch]
  - apiGroups: ["autoscaling"]
    resources: [horizontalpodautoscalers]
    verbs: [list, watch]
  - apiGroups: ["policy"]
    resources: [poddisruptionbudgets]
    verbs: [list, watch]
  - apiGroups: ["networking.k8s.io"]
    resources: [ingresses, networkpolicies]
    verbs: [list, watch]
  - apiGroups: ["storage.k8s.io"]
    resources: [storageclasses, volumeattachments]
    verbs: [list, watch]
  - apiGroups: ["certificates.k8s.io"]
    resources: [certificatesigningrequests]
    verbs: [list, watch]
  - apiGroups: ["admissionregistration.k8s.io"]
    resources: [mutatingwebhookconfigurations, validatingwebhookconfigurations]
    verbs: [list, watch]
  - apiGroups: ["coordination.k8s.io"]
    resources: [leases]
    verbs: [list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: { name: mdis-kube-state-metrics }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: mdis-kube-state-metrics }
subjects:
  - { kind: ServiceAccount, name: kube-state-metrics, namespace: hap-ops }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kube-state-metrics
  namespace: hap-ops
  labels: { app.kubernetes.io/name: kube-state-metrics }
spec:
  replicas: 1
  selector: { matchLabels: { app.kubernetes.io/name: kube-state-metrics } }
  template:
    metadata: { labels: { app.kubernetes.io/name: kube-state-metrics } }
    spec:
      # 不加 nodeSelector：无状态、无本地数据，落哪个节点都行
      serviceAccountName: kube-state-metrics
      containers:
        - name: kube-state-metrics
          image: registry.cn-hangzhou.aliyuncs.com/mdpublic/ops-allinone:1.5.8
          env: [{ name: ROLE, value: ksm }]
          ports:
            - { name: http-metrics, containerPort: 8080 }
            - { name: telemetry, containerPort: 8081 }
          readinessProbe:
            httpGet: { path: /, port: 8081 }
            initialDelaySeconds: 5
            timeoutSeconds: 5
          resources:
            requests: { cpu: 10m, memory: 64Mi }
            limits: { cpu: 200m, memory: 256Mi }
---
# ClusterIP 即可：Prometheus 在同集群内经 endpoints 自动发现，不必对外暴露
apiVersion: v1
kind: Service
metadata:
  name: kube-state-metrics
  namespace: hap-ops
  labels: { app.kubernetes.io/name: kube-state-metrics }
spec:
  selector: { app.kubernetes.io/name: kube-state-metrics }
  ports:
    - { name: http-metrics, port: 8080, targetPort: 8080 }
