diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..4d9dbf1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,34 @@ +name: Deploy Gitea Pages + +on: + push: + branches: [ main ] + paths: + - 'docs/**' + - 'mkdocs.yml' + workflow_dispatch: + + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt-get update -qq + apt-get install -y -qq python3-pip + pip3 install --break-system-packages mkdocs mkdocs-material + + - run: mkdocs build + + - name: Deploy to Gitea Pages + run: | + cd site + git init + git config user.name "gitea-actions" + git config user.email "actions@forteapps.net" + git add . + git commit -m "Deploy docs" + git push --force "https://x-token:${{ secrets.GITEA_TOKEN }}@git.forteapps.net/Forte/launchpad.git" HEAD:gitea-pages diff --git a/.gitignore b/.gitignore index c10a0fd..6f744e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs -.github/ private/ .helm/ temp/ @@ -15,4 +14,5 @@ CLAUDE.md .claude/ devbox.d/ devbox.lock -.devbox/ \ No newline at end of file +.devbox/ +bash.exe.stackdump \ No newline at end of file diff --git a/.project-standards.yaml b/.project-standards.yaml new file mode 100644 index 0000000..d7c9612 --- /dev/null +++ b/.project-standards.yaml @@ -0,0 +1,7 @@ +standards_version: "2025.1" +last_configured: "2026-04-04" +components: + github-pages: "2025.1" + github-pages-generator: "mkdocs" + github-pages-source: "docs/" + github-pages-theme: "material" diff --git a/README.md b/README.md index 15af8a7..1dea06c 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ ### For New Developers ```bash # 1. Clone repositories -git clone https://github.com/fortedigital/sturdy-adventure.git -git clone git@github.com:fortedigital/helm-values.git +git clone https://git.forteapps.net/Forte/launchpad.git +git clone ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git # 2. Read the guides # - Start: docs/GITOPS-ARCHITECTURE.md @@ -57,10 +57,10 @@ This repository contains the complete GitOps configuration for our Kubernetes cl ### What's Inside -- **Infrastructure Applications**: Traefik, Cert-Manager, Kyverno, Prometheus, Grafana, Loki, Sealed Secrets +- **Infrastructure Applications**: Traefik, Cert-Manager, Kyverno, Prometheus, Grafana, Loki, Tempo, Sealed Secrets - **Business Applications**: MCP10X, MusicMan, Dot-AI Stack, ArgoCD MCP - **Policies**: Kyverno security policies for secret management, namespace controls, pod verification -- **Monitoring**: Full observability stack with metrics, logs, and alerting +- **Monitoring**: Full observability stack with metrics, logs, traces, and alerting - **Secrets**: Sealed Secrets for secure Git storage ### Key Features @@ -72,7 +72,7 @@ This repository contains the complete GitOps configuration for our Kubernetes cl ✅ **Policy Enforcement**: Kyverno ensures security and compliance ✅ **Authentication**: Automatic sidecar injection (token & OIDC support) ✅ **TLS Everywhere**: Automatic Let's Encrypt certificates -✅ **Full Observability**: Prometheus, Grafana, Loki integration +✅ **Full Observability**: Prometheus, Grafana, Loki, Tempo integration --- @@ -91,9 +91,11 @@ This repository contains the complete GitOps configuration for our Kubernetes cl │ ├── prometheus.yaml │ ├── grafana.yaml │ ├── loki.yaml +│ ├── tempo.yaml │ ├── fluent-bit.yaml │ ├── trivy.yaml │ ├── sealedsecrets.yaml +│ ├── renovate.yaml │ └── values/ # Helm value overrides │ ├── apps/ # Business Applications @@ -136,9 +138,9 @@ This repository contains the complete GitOps configuration for our Kubernetes cl | Repository | Purpose | Who Edits | How Often | |------------|---------|-----------|-----------| -| **[sturdy-adventure](https://github.com/fortedigital/sturdy-adventure.git)** (this repo) | ArgoCD Applications, cluster resources | Platform / DevOps engineers | ✅ Often | -| **[forte-helm](https://github.com/snothub/forte-helm)** | Generic Helm chart templates | Platform engineers | ❌ Rarely | -| **[helm-values](git@github.com:fortedigital/helm-values.git)** | App-specific configuration & versions | Developers / CI pipelines | ✅ Sometimes | +| **[launchpad](https://git.forteapps.net/Forte/launchpad)** (this repo) | ArgoCD Applications, cluster resources | Platform / DevOps engineers | ✅ Often | +| **[forte-helm](https://git.forteapps.net/Forte/forte-helm)** | Generic Helm chart templates | Platform engineers | ❌ Rarely | +| **[helm-values](ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git)** | App-specific configuration & versions | Developers / CI pipelines | ✅ Sometimes | ### GitOps Workflow @@ -331,7 +333,10 @@ kubectl patch application myapp -n argocd \ | **Prometheus** | Metrics | `monitoring` | 1 | | **Grafana** | Dashboards | `monitoring` | 1 | | **Loki** | Logs | `monitoring` | 1 | +| **Tempo** | Distributed tracing | `monitoring` | 1 | | **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet | +| **OpenCost** | Cost monitoring | `monitoring` | 1 | +| **Renovate** | Dependency updates | `renovate` | CronJob | | **Trivy** | Vulnerability scanning | `trivy-system` | 1 | **Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components) @@ -470,10 +475,11 @@ Documentation lives in `docs/`. To update: - [Kyverno Documentation](https://kyverno.io/docs/) - [Traefik Documentation](https://doc.traefik.io/traefik/) - [Cert-Manager Documentation](https://cert-manager.io/docs/) +- [Grafana Tempo Documentation](https://grafana.com/docs/tempo/) - [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) ### Related Repositories -- [forte-helm](https://github.com/snothub/forte-helm) - Helm chart templates +- [forte-helm](https://github.com/fortedigital/forte-helm) - Helm chart templates - [helm-values](git@github.com:fortedigital/helm-values.git) - Application values --- diff --git a/_app-of-apps-eu.yaml b/_app-of-apps-eu.yaml index 311556d..f38ebde 100644 --- a/_app-of-apps-eu.yaml +++ b/_app-of-apps-eu.yaml @@ -18,7 +18,7 @@ metadata: spec: project: default source: - repoURL: git@github.com:fortedigital/sturdy-adventure.git + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD path: infra/overlays/eu destination: diff --git a/apps/base/argo-mcp.yaml b/apps/base/argo-mcp.yaml index f9eac29..30b2874 100644 --- a/apps/base/argo-mcp.yaml +++ b/apps/base/argo-mcp.yaml @@ -16,14 +16,14 @@ metadata: spec: project: default sources: - - repoURL: https://github.com/snothub/forte-helm + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git path: forteapp targetRevision: HEAD helm: valueFiles: - $values/argocd-mcp/values.yaml - - repoURL: git@github.com:fortedigital/helm-values.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git targetRevision: HEAD ref: values diff --git a/apps/base/mcp10x.yaml b/apps/base/mcp10x.yaml index 3ec00d5..984c63a 100644 --- a/apps/base/mcp10x.yaml +++ b/apps/base/mcp10x.yaml @@ -17,14 +17,14 @@ metadata: spec: project: default sources: - - repoURL: https://github.com/snothub/forte-helm + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git path: forteapp targetRevision: HEAD helm: valueFiles: - $values/mcp10x/values.yaml - - repoURL: git@github.com:fortedigital/helm-values.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git targetRevision: HEAD ref: values diff --git a/apps/base/mcpcoder.yaml b/apps/base/mcpcoder.yaml index cb98beb..7e4ef10 100644 --- a/apps/base/mcpcoder.yaml +++ b/apps/base/mcpcoder.yaml @@ -1,39 +1,42 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: mcpcoder + name: opencost namespace: argocd annotations: argocd.argoproj.io/sync-wave: "1" - notifications.argoproj.io/subscribe.on-sync-succeeded.slack: "" - notifications.argoproj.io/subscribe.on-sync-failed.slack: "" - notifications.argoproj.io/subscribe.on-degraded.slack: "" labels: - app.kubernetes.io/name: mcpcoder - app.kubernetes.io/part-of: apps + app.kubernetes.io/name: opencost + app.kubernetes.io/part-of: monitoring app.kubernetes.io/managed-by: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default - sources: - - repoURL: https://github.com/snothub/forte-helm - path: forteapp - targetRevision: HEAD - helm: - valueFiles: - - $values/mcpcoder/values.yaml - - repoURL: git@github.com:fortedigital/helm-values.git + sources: + - repoURL: https://opencost.github.io/opencost-helm-chart + chart: opencost + targetRevision: "1.42.0" + helm: + releaseName: opencost + valueFiles: + - $values/infra/values/opencost-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD ref: values destination: server: https://kubernetes.default.svc - namespace: mcpcoder + namespace: monitoring + syncPolicy: automated: prune: true selfHeal: true + allowEmpty: false syncOptions: - CreateNamespace=true + - Validate=true + - ServerSideApply=true diff --git a/apps/base/musicman.yaml b/apps/base/musicman.yaml index 08578cc..c29b9a2 100644 --- a/apps/base/musicman.yaml +++ b/apps/base/musicman.yaml @@ -17,14 +17,14 @@ metadata: spec: project: default sources: - - repoURL: https://github.com/snothub/forte-helm + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git path: forteapp targetRevision: HEAD helm: valueFiles: - $values/musicman/values.yaml - - repoURL: git@github.com:fortedigital/helm-values.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git targetRevision: HEAD ref: values diff --git a/bootstrap.sh b/bootstrap.sh index 89b3010..dc11d10 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -17,7 +17,7 @@ echo "Bootstrapping cluster: ${clusterName} (${CLUSTER})..." Bootstrap() { ArgoCd - Github +# Github } diff --git a/cluster-resources/gitea-backup-cronjob.yaml b/cluster-resources/gitea-backup-cronjob.yaml new file mode 100644 index 0000000..d05ec17 --- /dev/null +++ b/cluster-resources/gitea-backup-cronjob.yaml @@ -0,0 +1,88 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: gitea-backup + namespace: gitea +spec: + schedule: "0 3 * * *" # daily at 03:00 UTC + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 1800 + template: + spec: + restartPolicy: Never + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + # Must run on the same node as Gitea to share the RWO volume + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: gitea + topologyKey: kubernetes.io/hostname + initContainers: + - name: gitea-dump + image: gitea/gitea:1.25.4 + command: + - sh + - -c + - | + gitea dump \ + -c /data/gitea/conf/app.ini \ + -f /backup/gitea-dump.zip \ + -t /tmp/gitea-dump && \ + echo "Dump completed: $(ls -lh /backup/gitea-dump.zip)" + volumeMounts: + - name: data + mountPath: /data + readOnly: true + - name: backup + mountPath: /backup + - name: tmp + mountPath: /tmp/gitea-dump + containers: + - name: upload + image: minio/mc:latest + env: + - name: HOME + value: /tmp + command: + - sh + - -c + - | + mc alias set upcloud "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}" + + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + KEY="gitea-dump-${TIMESTAMP}.zip" + echo "Uploading ${KEY}..." + mc cp /backup/gitea-dump.zip "upcloud/${S3_BUCKET}/${KEY}" && \ + echo "Upload complete." + + # Prune backups older than 7 days + echo "Pruning backups older than 7 days..." + mc rm --older-than 7d --force "upcloud/${S3_BUCKET}/" 2>&1 || true + echo "Pruning complete." + envFrom: + - secretRef: + name: gitea-backup-s3 + volumeMounts: + - name: backup + mountPath: /backup + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: gitea-shared-storage + - name: backup + emptyDir: + sizeLimit: 5Gi + - name: tmp + emptyDir: + sizeLimit: 5Gi diff --git a/cluster-resources/gitea-ssh-ingressroute.yaml b/cluster-resources/gitea-ssh-ingressroute.yaml new file mode 100644 index 0000000..fb68b90 --- /dev/null +++ b/cluster-resources/gitea-ssh-ingressroute.yaml @@ -0,0 +1,13 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteTCP +metadata: + name: gitea-ssh + namespace: gitea +spec: + entryPoints: + - giteassh + routes: + - match: HostSNI(`*`) + services: + - name: gitea-ssh + port: 22 diff --git a/cluster-resources/network/deny-external-egress-trivy.yaml b/cluster-resources/network/deny-external-egress-trivy.yaml new file mode 100644 index 0000000..939aa11 --- /dev/null +++ b/cluster-resources/network/deny-external-egress-trivy.yaml @@ -0,0 +1,37 @@ +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: deny-external-egress + namespace: trivy-system + labels: + app.kubernetes.io/managed-by: argocd + app.kubernetes.io/part-of: network-policies +spec: + endpointSelector: {} + egress: + # Allow DNS resolution + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + - port: "53" + protocol: TCP + + # Allow cluster-internal traffic (RFC1918) + - toCIDR: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + + # Allow Trivy vulnerability DB downloads (ghcr.io OCI registry) + - toFQDNs: + - matchName: ghcr.io + - matchName: pkg-containers.githubusercontent.com + toPorts: + - ports: + - port: "443" + protocol: TCP diff --git a/cluster-resources/policies/auth-sidecar-injector.yaml b/cluster-resources/policies/auth-sidecar-injector.yaml index 3259d9d..0babdc2 100644 --- a/cluster-resources/policies/auth-sidecar-injector.yaml +++ b/cluster-resources/policies/auth-sidecar-injector.yaml @@ -10,7 +10,7 @@ metadata: policies.kyverno.io/severity: medium policies.kyverno.io/subject: Pod policies.kyverno.io/description: >- - Injects an auth sidecar container into Pods annotated with policies.forteapps.io/auth: "true". Supports three auth modes controlled by the policies.forteapps.io/auth-type annotation: "token" (default), "oidc", and "mcp". In token mode the sidecar reads credentials from a mounted Secret volume. In OIDC mode the sidecar uses OpenID Connect with authority and client-id provided via required annotations (policies.forteapps.io/auth-oidc-authority and policies.forteapps.io/auth-oidc-client-id) and secrets from an auth-oidc Secret. In MCP mode the sidecar implements OAuth 2.0 for MCP servers per RFC 9728 (Protected Resource Metadata) and RFC 7591 (Dynamic Client Registration), configured via policies.forteapps.io/auth-mcp-resource and policies.forteapps.io/auth-mcp-authority annotations. A NetworkPolicy is generated to restrict ingress to the sidecar port only. + Injects an auth sidecar container into Pods annotated with policies.forteapps.io/auth: "true". Supports three auth modes controlled by the policies.forteapps.io/auth-type annotation: "token" (default), "oidc", and "mcp". In token mode the sidecar reads credentials from a mounted Secret volume. In OIDC mode the sidecar uses OpenID Connect with authority and client-id provided via required annotations (policies.forteapps.io/auth-oidc-authority and policies.forteapps.io/auth-oidc-client-id) and secrets from an auth-oidc Secret. In MCP mode the sidecar implements OAuth 2.0 for MCP servers per RFC 9728 (Protected Resource Metadata) and RFC 7591 (Dynamic Client Registration), configured via policies.forteapps.io/auth-mcp-resource and policies.forteapps.io/auth-mcp-authority annotations. The sidecar port defaults to 9001 and can be overridden via the policies.forteapps.io/auth-port annotation. A NetworkPolicy is generated to restrict ingress to the sidecar port only. spec: background: false rules: @@ -119,21 +119,26 @@ spec: - name: appPort variable: jmesPath: request.object.spec.containers[?name != 'authn'] | [0].ports[0].containerPort || `3000` + - name: sidecarPort + variable: + jmesPath: to_number(request.object.metadata.annotations."policies.forteapps.io/auth-port" || '9001') mutate: patchStrategicMerge: spec: containers: - name: authn - image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/snothub/stunning-memory' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" + image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" ports: - - containerPort: 8080 + - containerPort: "{{ sidecarPort }}" name: auth protocol: TCP env: - name: AUTH_LISTEN_ADDR - value: ":8080" + value: ":{{ sidecarPort }}" - name: AUTH_UPSTREAM_URL value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-upstream-url\" || join('', ['http://localhost:', to_string(appPort)]) }}" + - name: AUTH_PUBLIC_PATHS + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-public-paths\" || '/healthz' }}" - name: AUTH_TOKEN_FILE value: "/etc/auth/tokens" - name: AUTH_MODE @@ -152,13 +157,13 @@ spec: readinessProbe: httpGet: path: /healthz - port: 8080 + port: "{{ sidecarPort }}" initialDelaySeconds: 2 periodSeconds: 5 livenessProbe: httpGet: path: /healthz - port: 8080 + port: "{{ sidecarPort }}" initialDelaySeconds: 5 periodSeconds: 10 securityContext: @@ -195,22 +200,25 @@ spec: - name: appPort variable: jmesPath: request.object.spec.containers[?name != 'authn'] | [0].ports[0].containerPort || `3000` + - name: sidecarPort + variable: + jmesPath: to_number(request.object.metadata.annotations."policies.forteapps.io/auth-port" || '9001') mutate: patchStrategicMerge: spec: containers: - name: authn - image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/snothub/stunning-memory' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" + image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" imagePullPolicy: Always ports: - - containerPort: 8080 + - containerPort: "{{ sidecarPort }}" name: auth protocol: TCP env: - name: AUTH_MODE value: "oidc" - name: AUTH_LISTEN_ADDR - value: ":8080" + value: ":{{ sidecarPort }}" - name: AUTH_LOG_LEVEL value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-log-level\" || 'info' }}" - name: AUTH_UPSTREAM_URL @@ -225,6 +233,8 @@ spec: value: "{{ regex_replace_all('https?://[^/]*', request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-callback-path\", '') }}" - name: AUTH_OIDC_SCOPES value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-scopes\" || 'openid,profile,email' }}" + - name: AUTH_PUBLIC_PATHS + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-public-paths\" || '/healthz' }}" - name: AUTH_OIDC_COOKIE_SECRET valueFrom: secretKeyRef: @@ -233,8 +243,8 @@ spec: - name: AUTH_OIDC_CLIENT_SECRET valueFrom: secretKeyRef: - name: auth-oidc - key: client-secret + name: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-credentials-secret\" || 'auth-oidc' }}" + key: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-credentials-secret-key\" || 'client-secret' }}" resources: limits: cpu: 50m @@ -245,13 +255,13 @@ spec: readinessProbe: httpGet: path: /healthz - port: 8080 + port: "{{ sidecarPort }}" initialDelaySeconds: 2 periodSeconds: 5 livenessProbe: httpGet: path: /healthz - port: 8080 + port: "{{ sidecarPort }}" initialDelaySeconds: 5 periodSeconds: 10 securityContext: @@ -283,22 +293,25 @@ spec: - name: appPort variable: jmesPath: request.object.spec.containers[?name != 'authn'] | [0].ports[0].containerPort || `3000` + - name: sidecarPort + variable: + jmesPath: to_number(request.object.metadata.annotations."policies.forteapps.io/auth-port" || '9001') mutate: patchStrategicMerge: spec: containers: - name: authn - image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/snothub/stunning-memory' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" + image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" imagePullPolicy: Always ports: - - containerPort: 8080 + - containerPort: "{{ sidecarPort }}" name: auth protocol: TCP env: - name: AUTH_MODE value: "mcp" - name: AUTH_LISTEN_ADDR - value: ":8080" + value: ":{{ sidecarPort }}" - name: AUTH_LOG_LEVEL value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-log-level\" || 'info' }}" - name: AUTH_UPSTREAM_URL @@ -307,8 +320,10 @@ spec: value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-resource\" }}" - name: AUTH_MCP_AUTHORIZATION_SERVERS value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-authority\" }}" + - name: AUTH_PUBLIC_PATHS + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-public-paths\" || '/healthz' }}" - name: AUTH_MCP_SCOPES_SUPPORTED - value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-scopes\" || 'read,write' }}" + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-scopes\" || 'profile' }}" resources: limits: cpu: 50m @@ -319,13 +334,106 @@ spec: readinessProbe: httpGet: path: /healthz - port: 8080 + port: "{{ sidecarPort }}" initialDelaySeconds: 2 periodSeconds: 5 livenessProbe: httpGet: path: /healthz - port: 8080 + port: "{{ sidecarPort }}" + initialDelaySeconds: 5 + periodSeconds: 10 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + - name: inject-sidecar-oauth + skipBackgroundRequests: true + match: + any: + - resources: + kinds: + - Pod + annotations: + policies.forteapps.io/auth: "true" + policies.forteapps.io/auth-type: "oauth" + exclude: + any: + - resources: + namespaces: + - kube-system + - kyverno + - argocd + - cert-manager + - monitoring + context: + - name: appPort + variable: + jmesPath: request.object.spec.containers[?name != 'authn'] | [0].ports[0].containerPort || `3000` + - name: sidecarPort + variable: + jmesPath: to_number(request.object.metadata.annotations."policies.forteapps.io/auth-port" || '9001') + mutate: + patchStrategicMerge: + spec: + containers: + - name: authn + image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}" + imagePullPolicy: Always + ports: + - containerPort: "{{ sidecarPort }}" + name: auth + protocol: TCP + env: + - name: AUTH_MODE + value: "oauth" + - name: AUTH_LISTEN_ADDR + value: ":{{ sidecarPort }}" + - name: AUTH_LOG_LEVEL + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-log-level\" || 'info' }}" + - name: AUTH_UPSTREAM_URL + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-upstream-url\" || join('', ['http://localhost:', to_string(appPort)]) }}" + - name: AUTH_OAUTH_AUTHORITY + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-authority\" }}" + - name: AUTH_OAUTH_CLIENT_ID + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-client-id\" }}" + - name: AUTH_OAUTH_SCOPES + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-scopes\" || 'openid,profile,email' }}" + - name: AUTH_OAUTH_DELEGATION_ENABLED + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-delegation-enabled\" || 'false' }}" + - name: AUTH_OAUTH_DELEGATION_CLIENT_ID + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-delegation-client-id\" || '' }}" + - name: AUTH_OAUTH_DELEGATION_SCOPES + value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-delegation-scopes\" || '' }}" + - name: AUTH_OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-credentials-secret\" || 'auth-oauth' }}" + key: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oauth-credentials-secret-key\" || 'client-secret' }}" + - name: AUTH_OAUTH_DELEGATION_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: auth-oauth + key: delegation-client-secret + resources: + limits: + cpu: 50m + memory: 64Mi + requests: + cpu: 10m + memory: 32Mi + readinessProbe: + httpGet: + path: /healthz + port: "{{ sidecarPort }}" + initialDelaySeconds: 2 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /healthz + port: "{{ sidecarPort }}" initialDelaySeconds: 5 periodSeconds: 10 securityContext: @@ -358,6 +466,10 @@ spec: operator: In value: - CREATE + context: + - name: sidecarPort + variable: + jmesPath: to_number(request.object.metadata.annotations."policies.forteapps.io/auth-port" || '9001') generate: synchronize: false apiVersion: networking.k8s.io/v1 @@ -376,5 +488,5 @@ spec: - Ingress ingress: - ports: - - port: 8080 + - port: "{{ sidecarPort }}" protocol: TCP diff --git a/cluster-resources/policies/deployment-verifier.yaml b/cluster-resources/policies/deployment-verifier.yaml deleted file mode 100644 index 8e827cc..0000000 --- a/cluster-resources/policies/deployment-verifier.yaml +++ /dev/null @@ -1,71 +0,0 @@ -apiVersion: kyverno.io/v1 -kind: ClusterPolicy -metadata: - name: require-deployment-owner -spec: - validationFailureAction: Audit - background: false - rules: - - name: check-pod-owner-is-replicaset-from-deployment - skipBackgroundRequests: true - match: - any: - - resources: - kinds: - - Pod - exclude: - any: - - resources: - namespaces: - - kube-system - - kyverno - - cert-manager - - monitoring - - argocd - - traefik-system - context: - - name: ownerReplicaSet - apiCall: - method: GET - urlPath: "/apis/apps/v1/namespaces/{{request.namespace}}/replicasets/{{request.object.metadata.ownerReferences[0].name}}" - jmesPath: "@" - preconditions: - all: - - key: "{{ request.object.metadata.ownerReferences || `[]` | [?kind=='ReplicaSet'] | length(@) }}" - operator: GreaterThanOrEquals - value: 1 - validate: - allowExistingViolations: true - message: "Pods must be created through a Deployment resource." - deny: - conditions: - any: - - key: "{{ownerReplicaSet.metadata.ownerReferences[0].kind}}" - operator: NotEquals - value: Deployment - - name: deny-pods-without-replicaset-owner - match: - any: - - resources: - kinds: - - Pod - exclude: - any: - - resources: - namespaces: - - kube-system - - kyverno - - cert-manager - - monitoring - - argocd - - traefik-system - skipBackgroundRequests: true - validate: - allowExistingViolations: true - message: "Direct pod creation is not allowed. Pods must come from a Deployment managed by ArgoCD." - deny: - conditions: - all: - - key: "{{ request.object.metadata.ownerReferences || `[]` | [?kind=='ReplicaSet'] | length(@) }}" - operator: LessThan - value: 1 diff --git a/cluster-resources/policies/keycloak-client-cloner.yaml b/cluster-resources/policies/keycloak-client-cloner.yaml new file mode 100644 index 0000000..d83c43c --- /dev/null +++ b/cluster-resources/policies/keycloak-client-cloner.yaml @@ -0,0 +1,37 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: keycloak-client-config-cloner +spec: + rules: + - name: clone-client-config-to-keycloak + skipBackgroundRequests: false + match: + any: + - resources: + kinds: + - Secret + selector: + matchLabels: + keycloak.forteapps.net/client-config: "true" + exclude: + any: + - resources: + namespaces: + - keycloak + generate: + apiVersion: v1 + kind: Secret + name: "{{request.object.metadata.name}}" + namespace: keycloak + synchronize: true + data: + metadata: + labels: + keycloak.forteapps.net/client-config: "true" + keycloak.forteapps.net/source-namespace: "{{request.object.metadata.namespace}}" + annotations: + keycloak.forteapps.net/source-name: "{{request.object.metadata.name}}" + keycloak.forteapps.net/source-namespace: "{{request.object.metadata.namespace}}" + data: "{{request.object.data}}" + type: "{{request.object.type}}" diff --git a/cluster-resources/policies/label-checker.yaml b/cluster-resources/policies/label-checker.yaml index 129007a..8a8efd3 100644 --- a/cluster-resources/policies/label-checker.yaml +++ b/cluster-resources/policies/label-checker.yaml @@ -26,6 +26,7 @@ spec: - monitoring - secrets - kyverno + - trivy-system match: any: - resources: diff --git a/cluster-resources/snothub-repo-credentials-sealed.yaml b/cluster-resources/snothub-repo-credentials-sealed.yaml deleted file mode 100644 index aba4b5f..0000000 --- a/cluster-resources/snothub-repo-credentials-sealed.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -apiVersion: bitnami.com/v1alpha1 -kind: SealedSecret -metadata: - creationTimestamp: null - name: snothub-repo-creds - namespace: argocd -spec: - encryptedData: - sshPrivateKey: AgCBd+i8jXBDwvWM0YC9OWvNTKyynVpW6hF0F3aH0GXBIYxzFo1g9kMajE/Ce3bTl8DiVs7VGzPdI6lmJtSC3+fVMm4wavDGrUUbhUHSR/HnEqq51NVjxU1Uj+VRz550z9r6gB0VLAcYqN0WLKd/4Qn7tvQOmKsaXMd5jsIYpSB8nZK87awZ5niFmT8DIKu2DOzoDBeN7Yqrf1aELq3m3kaDxGcDMYSglB0xRU3fF8FYr5inCic1eTrCUdfIwnrBSublNxI9FrGnlylRC40XCPxNpy6zyoQt7yeJTNgRSvtfceqXAwh25mcvga+BfDBTLPQ0EDbCoNDnE9EyMUa4kWcXBTFZ1Qx5NGRz8HAjWoYDbuNOonl9ik5dvv9UKKA+/04ZEuPjsbdkBq9xKuowx0zLL1IVv/jeuViFdvNY6LKNu/hLwMn8aMlTLlIkB+1fsIqV7Vkva9Vk7IXNz575lMwIGUTo1dsK9FQ5+uIe2bsRnY8RJ3lpyndZQ1HDPh7P7KZLZPH8fUuAHH1UF74njMQyve79zCRcisBAewpXdq4UsYAYUQOluS1Ak+sFcIdQ0jHjfklGCcJnTvMyO7obIsPQSv39/bsCqQX6uisrTzcb0s7wnbzhcxf1gm7IyZMIhi2Vub3GoLCIMnb6ViO8k+itLUa5eZpoEeg+BpP1mgL7O2nVfrZYgueULgMSvN89ct/THITsFAR8614An3DCHSnZLv5ZmY7yC6rmO405IlrnjvfFqEt9MdqTgR7uTYDSdI7UjIFJp3rchzikF1pSDMu/siKmD/Vi9+S0KqBcSENz7EUppVuYIk0aRBqHZ9Awoe0qPIjp1AMg947FjIYXzkGk3Kz0P85fGwkktyfHNZdIrXvpDAIP2739Mr7Hde+EHpuajrhuBgozfless5PqLFfGWFnt5COW2HrdSrvrY56MXuJUfiV0nq3eEhCju9f/cA41VbxfO0Hj+KMCjxiL+MCgIt1eWD24P7GQHb0D7+JiuwqgcH0ZqXRaKFYSOJ3/U1o9RRX9v5yebNTm8ErQTSnIN5bNRgE+t8CrTulGVcpaL/rW+XW8cO8MXLC/R4eeNwVjoiK4yXSHpSjf5dF9MZ6bS6SSZPMOa2besEzzQIj0c/EkUsp/GmIF1utDbJVI2VZLFGklwRo92OJs3URXNCqbzzgte43Y9dJp2VdWyi3Zx32pXVWaNdOzeifVOASP7ags7Kbdfvoaa5CmTu17iRUHCOv6164/KFeDCfY= - type: AgCIKe+SzLHIp+6gjOVbD7wcpZkeg5UwgXabFjtonavkPbzX+txHWN7IH2HbDmd7TdpgGFiqMGFSQC8mOVXnj+Pw1XnI8trH6wavvKjQ1SsRWteB8o7lGe2PcG2h2v5yW2vk5rrmuB0ehogeJez3ynlk508HguRtidzxnKdZyc6SHOh2hbWq1clspJicREsHlz3Rfn7upOSUyFmx+Tilfnjuom5FFYGBNZt0aEjaW5S7fjcYJBTEerjGjTz3vUs2DK3C5ymyCKKasA628fVZ20uIZhmn5NUGGK6bKDusFOQhYjRnxRYwS2fToBHiVfC8wo7bWW8ZwOrbguvedJU2q9pwjKvXy9upw8Ra3EcYzXASqwI6rqUQv6htGqzTYtVkRsdVxaQqITc3FYrhKIroru4Iup4xYtcVojppz0+HQxiv5WtxosVtfXXX4Zj4spr9ThoZgzLe3ebILEIK3wJNmK/AxV80NXq7JOI6gaiLK/fbm7gd9G7oW9VuM50OJELDmR4jU2k1KSx6sD5lB+c+Ajp1iw+iS9ETD/je93+eyBRKGM9fqP0+DpNsJBBlVLuWK/BwidBN186pFi4DlMzo13Wd0zYKQce4NVrf5s4f7T01KeBU+nnvvcuI+rW7tsY3R18zgP9D9uX2E5066qJQdrFov2YuXwovS4hCQDttIfz9YoGUKKQ3PObWUVynhtBptoGbhRw= - url: AgCjVnAWNarZplxbAurz++eBiKXIuYUcoleDcsrMdwdflBOOf6ayguCr7CDHzUbuKFnlNKoFvIvjUOqFb1v5Yy4iWju3ajvlo+ncEIezxetYOmQSft3nSeOD3+RCZn4Qf4K2C1D113IyPCo7T2h01KuVLWOItfuWuVwFDpLTvmKfTRAs8eP1L4ohvAcY0/J5mAmi7tXV2hN1r9R2MEuvHOusujtrRqewXHhzBksSb9/wxilruU/BHQAhYKeKHzi7QoIOeXJTnGEYxuTvskwKQhschIOIBPAOLaUbgKkuHuDIf8y1Gv2b8ENu5uNvTb/ZD43jtmx9P4pS+Hwc7OW411TrkRO7XbV7qo/PqYGYpkKYDK4g7ONGnzrsmXbSmip34vXll/jAknY61QQ6D6JbbONw/psX72p+ZFvOedhlKbHRuUnDYXyQgKFWBODLb63RaMYai79qbv8mJcwDLJaPXYXpwLumHeZX91uPPjahxfvOe8VoryTpvHIbxJO85VJJ1q+7uyFy6h7LVLhifYSbb+M55p/e5Ds5gNcgUv6npPUHdbf0yjYbT1tXGjhaqk4Tx93WoOKPQ/j+nPB41akooY0YfL/ZTDzr2iCMByrx/uPQz3JE+m7VrH5BZyYjj9sSASVsULabJGFiSuGpD+u/lcUNJW+WHQetMEU7+wNo/WB3E5iG9J8qPSPFcTiTslEa8cvGPNtNgyCQ2PphlinscQsvcAQ+ALtEd1dWXPhIGbF8udK6Wx6NYt8= - template: - metadata: - creationTimestamp: null - labels: - argocd.argoproj.io/secret-type: repository - name: snothub-repo-creds - namespace: argocd - type: Opaque diff --git a/docs/DEVELOPER-GUIDE.md b/docs/DEVELOPER-GUIDE.md index 364d1dd..27b7ddc 100644 --- a/docs/DEVELOPER-GUIDE.md +++ b/docs/DEVELOPER-GUIDE.md @@ -9,6 +9,7 @@ - [Updating an Existing Application](#updating-an-existing-application) - [Working with Secrets](#working-with-secrets) - [Enabling Authentication for Applications](#enabling-authentication-for-applications) +- [Adding a New Keycloak Client](#adding-a-new-keycloak-client) - [Troubleshooting](#troubleshooting) - [Best Practices](#best-practices) @@ -89,21 +90,21 @@ If you do need cluster access, install: You'll need read/write access to these repositories: -1. **sturdy-adventure** (Config repo) +1. **launchpad** (Config repo) ```bash - git clone https://github.com/fortedigital/sturdy-adventure.git - cd sturdy-adventure + git clone https://git.forteapps.net/Forte/launchpad.git + cd launchpad ``` 2. **helm-values** (Values repo) ```bash - git clone git@github.com:fortedigital/helm-values.git + git clone https://git.forteapps.net/Forte/helm-prod-values.git cd helm-values ``` 3. **forte-helm** (Chart repo - read-only for most developers) ```bash - git clone https://github.com/snothub/forte-helm.git + git clone https://git.forteapps.net/Forte/forte-helm.git cd forte-helm ``` @@ -132,9 +133,9 @@ mkdir -p ~/dev/k8s cd ~/dev/k8s # Clone repositories -git clone https://github.com/fortedigital/sturdy-adventure.git launchpad -git clone git@github.com:fortedigital/helm-values.git helm-prod-values -git clone https://github.com/snothub/forte-helm.git forte-helm +git clone https://git.forteapps.net/Forte/launchpad.git launchpad +git clone https://git.forteapps.net/Forte/helm-prod-values helm-prod-values +git clone https://git.forteapps.net/Forte/forte-helm forte-helm # Your folder structure: # ~/dev/k8s/ @@ -201,7 +202,7 @@ Our setup uses three repositories: |------------|---------|-----------|-----------| | **forte-helm** | Helm chart templates (generic, reusable) | Platform engineers | ❌ Rarely | | **helm-values** | Application configuration (image tag, env vars) | Developers / CI pipelines | ✅ Sometimes | -| **sturdy-adventure** | ArgoCD Applications (what gets deployed) | Platform / DevOps engineers | ✅ Per new app | +| **launchpad** | ArgoCD Applications (what gets deployed) | Platform / DevOps engineers | ✅ Per new app | ### Example: Deploying "myapp" @@ -236,7 +237,7 @@ app: value: https://api.example.com ``` -#### Repository: `sturdy-adventure` (ArgoCD Application) +#### Repository: `launchpad` (ArgoCD Application) ```yaml # apps/myapp.yaml # Tells ArgoCD to deploy your app @@ -247,7 +248,7 @@ metadata: namespace: argocd spec: sources: - - repoURL: https://github.com/snothub/forte-helm + - repoURL: https://github.com/fortedigital/forte-helm path: forteapp helm: valueFiles: @@ -386,7 +387,7 @@ git push ### Step 3: Create ArgoCD Application Manifest -In the `sturdy-adventure` repository, create `apps/hello-world.yaml`: +In the `launchpad` repository, create `apps/hello-world.yaml`: ```yaml apiVersion: argoproj.io/v1alpha1 @@ -411,7 +412,7 @@ spec: sources: # Source 1: Helm chart templates - - repoURL: https://github.com/snothub/forte-helm + - repoURL: https://github.com/fortedigital/forte-helm path: forteapp targetRevision: HEAD helm: @@ -1025,7 +1026,7 @@ policies.forteapps.io/auth-upstream-url: "http://localhost:3000" #### Sidecar Configuration The auth sidecar container: -- **Image**: `ghcr.io/snothub/stunning-memory:latest` +- **Image**: `ghcr.io/fortedigital/auth-sidecar:latest` - **Port**: 8080 - **Resources**: 10m CPU / 32Mi memory (requests), 50m CPU / 64Mi memory (limits) - **Health checks**: `/healthz` endpoint @@ -1247,6 +1248,202 @@ kubectl logs -n myapp -c authn --- +## Adding a New Keycloak Client + +There are two ways to add an OIDC client, depending on your use case: + +| Method | Best for | Who edits the infra repo? | +|--------|----------|--------------------------| +| **Self-service** (recommended) | New apps that deploy their own resources | App developer — no infra changes needed | +| **Legacy (realm JSON)** | Existing clients already defined in forte-realm.json (e.g., Gitea) | Platform engineer | + +Both methods are served by the **Keycloak Client Registrar** CronJob, which runs every 2 minutes. + +### Self-Service OIDC Client Registration + +This is the recommended flow for new applications. Your app deploys a labeled config Secret in its own namespace; the platform handles everything else. + +#### How It Works + +1. You deploy a Secret with label `keycloak.forteapps.net/client-config: "true"` containing a `client.json` definition +2. A **Kyverno ClusterPolicy** (`keycloak-client-config-cloner`) clones it to the `keycloak` namespace +3. The **Client Registrar CronJob** picks it up within 2 minutes: + - Registers (or updates) the client in Keycloak + - Fetches the auto-generated client secret + - Creates a credential Secret in your app's namespace + - Annotates the config Secret with sync status + +#### Step 1: Create the Config Secret + +Deploy this Secret in your application's namespace (e.g., as part of your Helm chart or Kustomize overlay): + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-client-myapp + namespace: myapp + labels: + keycloak.forteapps.net/client-config: "true" +stringData: + client.json: | + { + "clientId": "myapp", + "name": "My Application", + "redirectUris": ["https://myapp.forteapps.net/*"], + "webOrigins": ["https://myapp.forteapps.net"], + "defaultClientScopes": ["openid", "email", "profile"], + "protocolMappers": [], + "secret": { + "namespace": "myapp", + "name": "myapp-oidc-credentials", + "keys": { "clientId": "client-id", "clientSecret": "client-secret" } + } + } +``` + +**`client.json` fields**: + +| Field | Required | Description | +|-------|----------|-------------| +| `clientId` | Yes | Keycloak client ID | +| `name` | Yes | Display name in Keycloak | +| `redirectUris` | Yes | Allowed redirect URIs | +| `webOrigins` | Yes | Allowed web origins (CORS) | +| `defaultClientScopes` | No | Scopes (default: `["openid", "email", "profile"]`) | +| `protocolMappers` | No | Custom claim mappers (default: `[]`) | +| `secret.namespace` | No | Namespace for the credential Secret (default: source namespace) | +| `secret.name` | No | Name of the credential Secret (default: `-oidc-credentials`) | +| `secret.keys.clientId` | No | Key name for client ID in credential Secret (default: `client-id`) | +| `secret.keys.clientSecret` | No | Key name for client secret in credential Secret (default: `client-secret`) | + +#### Step 2: Reference the Credential Secret + +In your application's deployment config, reference the credential Secret that the registrar creates: + +```yaml +env: +- name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: myapp-oidc-credentials + key: client-id +- name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: myapp-oidc-credentials + key: client-secret +``` + +#### Step 3: Deploy and Wait + +Commit and push your changes. The credential Secret will appear within 2 minutes: + +```bash +# Watch for the credential Secret to be created +kubectl get secret myapp-oidc-credentials -n myapp -w + +# Check registrar logs +kubectl logs -n keycloak job/$(kubectl get jobs -n keycloak --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}') + +# Check sync status on the config Secret +kubectl get secret keycloak-client-myapp -n keycloak -o jsonpath='{.metadata.annotations}' +``` + +#### Change Detection + +The registrar computes a SHA-256 hash of `client.json` and stores it as an annotation. On subsequent runs, it skips processing if: +- The hash hasn't changed, AND +- The credential Secret already exists in the target namespace + +To force a re-sync, update any field in `client.json` (e.g., add a trailing space to `name`). + +### Legacy Method: Realm JSON + +Existing clients (like Gitea) are defined directly in `forte-realm.json` inside `keycloak-values.yaml`. The registrar syncs their secrets via client attributes. + +#### Step 1: Add Client to Realm Config + +In `infra/values/keycloak-values.yaml`, add a new entry to the `clients` array in `forte-realm.json`: + +```json +{ + "clientId": "myapp", + "name": "My Application", + "enabled": true, + "protocol": "openid-connect", + "clientAuthenticatorType": "client-secret", + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "publicClient": false, + "redirectUris": ["https://myapp.forteapps.net/*"], + "webOrigins": ["https://myapp.forteapps.net"], + "defaultClientScopes": ["openid", "email", "profile"], + "attributes": { + "k8s.secret.sync": "true", + "k8s.secret.namespace": "myapp", + "k8s.secret.name": "myapp-oidc-credentials", + "k8s.secret.client-id-key": "key", + "k8s.secret.client-secret-key": "secret" + } +} +``` + +**Important**: +- Do **NOT** include a `"secret"` field — Keycloak generates one automatically +- The `attributes` block tells the registrar where to create the K8s Secret +- Set `client-id-key` / `client-secret-key` to match what the consuming app expects (defaults: `client-id` / `client-secret`) + +#### Step 2: Reference the Secret in Your Application + +```yaml +existingSecret: myapp-oidc-credentials +``` + +#### Step 3: Commit and Push + +```bash +cd ~/dev/k8s/launchpad +git add infra/values/keycloak-values.yaml +git commit -m "Add myapp Keycloak client with auto-sync" +git push +``` + +ArgoCD will sync the Keycloak config, and the registrar CronJob will pick up the new client within 2 minutes. + +#### Legacy Sync Attribute Reference + +| Attribute | Required | Default | Description | +|-----------|----------|---------|-------------| +| `k8s.secret.sync` | Yes | — | Set to `"true"` to enable syncing | +| `k8s.secret.namespace` | Yes | — | Target K8s namespace for the secret | +| `k8s.secret.name` | Yes | — | Name of the K8s Secret to create | +| `k8s.secret.client-id-key` | No | `client-id` | Field name for the client ID in the K8s Secret | +| `k8s.secret.client-secret-key` | No | `client-secret` | Field name for the client secret in the K8s Secret | + +### Retrieving Secrets for External Deployments + +The registrar always writes a **central copy** of every synced secret to the `secrets` namespace, in addition to the target namespace. This allows operators to retrieve client credentials for applications deployed outside this cluster: + +```bash +# View the central copy +kubectl get secret gitea-oidc-credentials -n secrets -o yaml + +# Extract the client secret for use elsewhere +kubectl get secret myapp-oidc-credentials -n secrets \ + -o jsonpath='{.data.client-secret}' | base64 -d +``` + +### Registrar Behavior Notes + +- The registrar runs as a CronJob every 2 minutes (`concurrencyPolicy: Forbid`) +- If the target namespace doesn't exist, the target write is skipped with a warning (the central copy still happens) +- A central copy is **always** written to the `secrets` namespace for every synced client +- The registrar uses the `keycloak-credentials` secret for admin authentication +- Created secrets have the label `app.kubernetes.io/managed-by: keycloak-client-registrar` + +--- + ## Troubleshooting ### Application Not Deploying @@ -1579,4 +1776,4 @@ Now that you understand the basics: - Docs: [Full documentation index](README.md) - Help: Contact platform team -**Last Updated**: 2026-03-16 +**Last Updated**: 2026-04-16 diff --git a/docs/GITOPS-ARCHITECTURE.md b/docs/GITOPS-ARCHITECTURE.md index f126421..e663979 100644 --- a/docs/GITOPS-ARCHITECTURE.md +++ b/docs/GITOPS-ARCHITECTURE.md @@ -21,7 +21,7 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where - **Deployment Pattern**: App-of-Apps - **Secret Management**: Sealed Secrets (kubeseal) - **Ingress**: Traefik with Let's Encrypt TLS -- **Monitoring**: Prometheus + Grafana + Loki + Fluent-Bit +- **Monitoring**: Prometheus + Grafana + Loki + Tempo + Fluent-Bit - **Policy Engine**: Kyverno - **Notifications**: Slack integration for sync status @@ -53,8 +53,8 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where ┌────────────────────────────────┐ │ Config Repository │ │ (ArgoCD Applications) │ - │ github.com/snothub/ │ - │ sturdy-adventure │ + │ git.forteapps.net/Forte/ │ + │ launchpad │ └────────────────────────────────┘ │ │ @@ -83,6 +83,7 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where │ │ - Prometheus │ │ │ │ - Grafana │ │ │ │ - Loki │ │ + │ │ - Tempo │ │ │ │ - Fluent-Bit │ │ │ └──────────────────────────┘ │ │ │ @@ -108,12 +109,12 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where ## Repository Structure ### 1. **Config Repository** (Current Repo) -**Repository**: `https://github.com/fortedigital/sturdy-adventure.git` +**Repository**: `https://git.forteapps.net/Forte/launchpad` **Purpose**: GitOps configuration - ArgoCD Applications and cluster resources **Location**: `C:\dev\k8s\launchpad` ``` -sturdy-adventure/ +launchpad/ ├── bootstrap.sh # Cluster initialization script ├── _app-of-apps.yaml # Root ArgoCD Application (App-of-Apps pattern) │ @@ -127,6 +128,7 @@ sturdy-adventure/ │ ├── prometheus.yaml │ ├── grafana.yaml │ ├── loki.yaml +│ ├── tempo.yaml │ ├── fluent-bit.yaml │ ├── trivy.yaml │ ├── sealedsecrets.yaml @@ -136,6 +138,7 @@ sturdy-adventure/ │ ├── prometheus-values.yaml │ ├── grafana-values.yaml │ ├── loki-values.yaml +│ ├── tempo-values.yaml │ └── fluent-bit-values.yaml │ ├── apps/ # Business Application ArgoCD manifests @@ -150,7 +153,6 @@ sturdy-adventure/ │ ├── letsencrypt-issuer.yaml # Let's Encrypt ClusterIssuer │ ├── kyverno-config.yaml │ ├── argocd-notifications-secret-sealed.yaml -│ ├── snothub-repo-credentials-sealed.yaml │ ├── forte10x-repo-credentials-sealed.yaml │ ├── mcp10x-repo-credentials-sealed.yaml │ └── policies/ # Kyverno policies @@ -188,7 +190,7 @@ sturdy-adventure/ --- ### 2. **Helm Charts Repository** -**Repository**: `https://github.com/snothub/forte-helm` +**Repository**: `https://github.com/fortedigital/forte-helm` **Purpose**: Reusable Helm chart templates for Forte applications **Location**: `C:\dev\k8s\forte-helm` @@ -302,6 +304,7 @@ _app-of-apps.yaml (Root) │ ├── kyverno │ ├── prometheus │ ├── grafana + │ ├── tempo │ └── ... (other infra apps) │ └── enterprise-apps (manages apps/) @@ -343,7 +346,7 @@ Applications like `mcp10x` and `musicman` use multiple sources: ```yaml spec: sources: - - repoURL: https://github.com/snothub/forte-helm + - repoURL: https://github.com/fortedigital/forte-helm path: forteapp # Helm chart templates helm: valueFiles: @@ -402,7 +405,7 @@ jobs: 1. **Config Repo Change**: - Developer updates `apps/myapp.yaml` - - Pushes to `sturdy-adventure` repo + - Pushes to `launchpad` repo - ArgoCD detects change (60s reconciliation) - Syncs application to cluster @@ -489,7 +492,6 @@ git commit -m "Add app credentials" **Private Repository Credentials** stored as SealedSecrets: ```yaml -# cluster-resources/snothub-repo-credentials-sealed.yaml # cluster-resources/forte10x-repo-credentials-sealed.yaml ``` @@ -528,8 +530,9 @@ annotations: 1. **Prometheus**: Metrics collection and storage 2. **Grafana**: Metrics visualization and dashboards 3. **Loki**: Log aggregation -4. **Fluent-Bit**: Log shipping from pods to Loki -5. **Trivy**: Container vulnerability scanning +4. **Tempo**: Distributed tracing (OTLP) +5. **Fluent-Bit**: Log shipping from pods to Loki +6. **Trivy**: Container vulnerability scanning ### Slack Notifications @@ -558,7 +561,7 @@ Notifications include: **Rebuild Process**: 1. Provision new Kubernetes cluster -2. Clone `sturdy-adventure` repository +2. Clone `launchpad` repository 3. Run `./bootstrap.sh` 4. ArgoCD installs and syncs all applications 5. Manually recreate unsealed secrets and seal them diff --git a/docs/OPERATIONS-RUNBOOK.md b/docs/OPERATIONS-RUNBOOK.md index 0425b0b..9ff6f9f 100644 --- a/docs/OPERATIONS-RUNBOOK.md +++ b/docs/OPERATIONS-RUNBOOK.md @@ -51,8 +51,8 @@ kubectl get nodes ```bash # 1. Clone config repository -git clone https://github.com/fortedigital/sturdy-adventure.git -cd sturdy-adventure +git clone https://git.forteapps.net/Forte/launchpad +cd launchpad # 2. Set cluster name (optional) export CLUSTER_NAME="prod-cluster-01" @@ -130,10 +130,10 @@ Generate a dedicated SSH key for ArgoCD without a passphrase (required for autom ```bash # Generate ED25519 key (recommended - smaller and more secure) -ssh-keygen -t ed25519 -C "argocd-deploy-key-sturdy-adventure" -f argocd-deploy-key -N "" +ssh-keygen -t ed25519 -C "argocd-deploy-key-launchpad" -f argocd-deploy-key -N "" # Or RSA key if ED25519 is not supported -ssh-keygen -t rsa -b 4096 -C "argocd-deploy-key-sturdy-adventure" -f argocd-deploy-key -N "" +ssh-keygen -t rsa -b 4096 -C "argocd-deploy-key-launchpad" -f argocd-deploy-key -N "" ``` This creates two files: @@ -148,7 +148,7 @@ This creates two files: ``` 2. Go to GitHub repository settings: - - Navigate to: `https://github.com/fortedigital/sturdy-adventure/settings/keys` + - Navigate to: `https://git.forteapps.net/Forte/launchpad/settings/keys` - Or: Repository → Settings → Deploy keys 3. Click **"Add deploy key"** @@ -169,78 +169,33 @@ This creates two files: Add the private key to ArgoCD as a repository secret: +Save the following file in private/ (gitignored) folder as secret.yaml ```bash -# Create secret for sturdy-adventure repository -kubectl create secret generic repo-sturdy-adventure \ - --from-file=sshPrivateKey=argocd-deploy-key \ - --namespace=argocd \ - --dry-run=client -o yaml | kubectl apply -f - - -# Label it for ArgoCD to recognize -kubectl label secret repo-sturdy-adventure \ - -n argocd \ - argocd.argoproj.io/secret-type=repository - -# Add repository annotations -kubectl annotate secret repo-sturdy-adventure \ - -n argocd \ - managed-by=argocd.argoproj.io + apiVersion: v1 + kind: Secret + metadata: + name: forte-helm-repo + namespace: argocd + labels: + argocd.argoproj.io/secret-type: repository + stringData: + type: git + url: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git + sshPrivateKey: | + + project: default ``` - -Alternatively, create a complete repository secret with all metadata: - +Seal the secret using `kubeseal` command ```bash -kubectl apply -f - < secrets/forte-helm-repo-secret-sealed.yaml ``` **Step 4: Register Repository in ArgoCD** -Add the repository to ArgoCD's configuration: - -```bash -# Via kubectl (recommended for GitOps) -kubectl apply -f - <> ~/.ssh/known_hosts # Or disable strict host key checking (less secure) -kubectl patch secret repo-sturdy-adventure -n argocd \ +kubectl patch secret repo-launchpad -n argocd \ --type merge \ -p '{"stringData":{"insecure":"true"}}' ``` @@ -381,7 +336,7 @@ kubectl patch secret repo-sturdy-adventure -n argocd \ kubectl logs -n argocd deployment/argocd-repo-server # Refresh repository connection -kubectl delete secret repo-sturdy-adventure -n argocd +kubectl delete secret repo-launchpad -n argocd # Recreate secret (see Step 3 above) # Restart ArgoCD components @@ -391,21 +346,21 @@ kubectl rollout restart deployment argocd-application-controller -n argocd #### Multiple Repository Setup -For the three-repository pattern (sturdy-adventure, forte-helm, helm-values): +For the three-repository pattern (launchpad, forte-helm, helm-values): ```bash -# 1. sturdy-adventure (main config repo) -ssh-keygen -t ed25519 -C "argocd-sturdy-adventure" -f key-sturdy -N "" -# Add key-sturdy.pub to: https://github.com/fortedigital/sturdy-adventure/settings/keys +# 1. launchpad (main config repo) +ssh-keygen -t ed25519 -C "argocd-launchpad" -f key-sturdy -N "" +# Add key-sturdy.pub to: https://git.forteapps.net/Forte/launchpad/settings/keys # 2. helm-values (private values repo) ssh-keygen -t ed25519 -C "argocd-helm-values" -f key-helm-values -N "" # Add key-helm-values.pub to: https://github.com/fortedigital/helm-values/settings/keys -# 3. forte-helm is public - no key needed (use HTTPS) +# 3. forte-helm (private helm charts repo) # Create secrets -kubectl create secret generic repo-sturdy-adventure \ +kubectl create secret generic repo-launchpad \ --from-file=sshPrivateKey=key-sturdy \ --namespace=argocd --dry-run=client -o yaml | \ kubectl label --local -f - argocd.argoproj.io/secret-type=repository --dry-run=client -o yaml | \ @@ -430,9 +385,9 @@ If you're currently using HTTPS and want to switch to SSH: # 2. Update all Application manifests # Change from: -# repoURL: https://github.com/fortedigital/sturdy-adventure.git +# repoURL: https://git.forteapps.net/Forte/launchpad # To: -# repoURL: git@github.com:fortedigital/sturdy-adventure.git +# repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git # 3. Update and commit find . -name "*.yaml" -type f -exec sed -i 's|https://github.com/fortedigital/|git@github.com:fortedigital/|g' {} + @@ -999,6 +954,33 @@ curl -G -s 'http://localhost:3100/loki/api/v1/query_range' \ --data-urlencode 'start=1h' | jq ``` +### Tempo Traces + +```bash +# Port forward to Tempo query API +kubectl port-forward -n monitoring svc/tempo 3200:3200 + +# Access: http://localhost:3200 +``` + +**Query traces via Grafana:** +1. Open Grafana → Explore +2. Select Tempo datasource +3. Use TraceQL or search by service name + +**Verify Traefik is sending traces:** +```bash +# Check Traefik logs for OTLP export errors +kubectl logs -n traefik-system -l app.kubernetes.io/name=traefik | grep -i "traces export" + +# Check Tempo is receiving data +kubectl logs -n monitoring -l app.kubernetes.io/name=tempo | grep "receiver" +``` + +**Trace-to-log correlation:** +- Click a trace span in Grafana → linked Loki logs appear (by namespace, pod, container) +- Trace-to-metrics links to Prometheus by service name + ### Fluent-Bit Log Shipping Verify Fluent-Bit is shipping logs: diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index c556c88..5d72b57 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -29,7 +29,9 @@ | **Secret Management** | Sealed Secrets (Bitnami) | | **Monitoring** | Prometheus + Grafana | | **Logging** | Loki + Fluent-Bit | +| **Tracing** | Tempo (OTLP) | | **Container Scanning** | Trivy | +| **Version Control** | Gitea | ### Network Architecture @@ -60,14 +62,14 @@ Internet ## Repository Reference -### Config Repository: `sturdy-adventure` +### Config Repository: `launchpad` -**URL**: `https://github.com/fortedigital/sturdy-adventure.git` +**URL**: `https://git.forteapps.net/Forte/launchpad` #### Directory Structure ``` -sturdy-adventure/ +launchpad/ ├── bootstrap.sh # Cluster initialization script ├── _app-of-apps.yaml # Root ArgoCD Application │ @@ -81,16 +83,24 @@ sturdy-adventure/ │ ├── prometheus.yaml │ ├── grafana.yaml │ ├── loki.yaml +│ ├── tempo.yaml │ ├── fluent-bit.yaml │ ├── trivy.yaml +│ ├── gitea.yaml +│ ├── gitea-actions.yaml │ ├── sealedsecrets.yaml │ ├── secrets.yaml +│ ├── renovate.yaml │ └── values/ │ ├── argocd-values.yaml │ ├── prometheus-values.yaml │ ├── grafana-values.yaml │ ├── loki-values.yaml -│ └── fluent-bit-values.yaml +│ ├── tempo-values.yaml +│ ├── gitea-values.yaml +│ ├── gitea-actions-values.yaml +│ ├── fluent-bit-values.yaml +│ └── renovate-values.yaml │ ├── apps/ # Business applications │ ├── mcp10x.yaml @@ -104,7 +114,6 @@ sturdy-adventure/ │ ├── letsencrypt-issuer.yaml │ ├── kyverno-config.yaml │ ├── argocd-notifications-secret-sealed.yaml -│ ├── snothub-repo-credentials-sealed.yaml │ ├── forte10x-repo-credentials-sealed.yaml │ ├── mcp10x-repo-credentials-sealed.yaml │ └── policies/ @@ -114,11 +123,14 @@ sturdy-adventure/ │ ├── replicaset-cleaner.yaml │ ├── default-ns-blocker.yaml │ ├── secret-cloner.yaml +│ ├── keycloak-client-cloner.yaml │ └── auth-sidecar-injector.yaml │ ├── secrets/ # Application secrets (sealed) │ ├── argocd-mcp-credentials.yaml │ ├── dot-ai-secrets.yaml +│ ├── gitea-credentials-sealed.yaml +│ ├── gitea-runner-token-sealed.yaml │ ├── mcp10x-credentials-sealed.yaml │ └── musicman-credentials.yaml │ @@ -162,7 +174,7 @@ metadata: spec: project: default source: - repoURL: git@github.com:fortedigital/sturdy-adventure.git + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git path: infra destination: server: https://kubernetes.default.svc @@ -177,7 +189,7 @@ spec: ### Helm Charts Repository: `forte-helm` -**URL**: `https://github.com/snothub/forte-helm` +**URL**: `https://github.com/fortedigital/forte-helm` #### Chart: `forteapp` @@ -513,7 +525,7 @@ spec: # Multi-source configuration sources: - - repoURL: https://github.com/snothub/forte-helm + - repoURL: https://github.com/fortedigital/forte-helm path: forteapp targetRevision: HEAD helm: @@ -704,6 +716,7 @@ kubeStateMetrics: **Datasources**: - Prometheus - Loki +- Tempo ### Loki @@ -721,6 +734,45 @@ promtail: enabled: false # Using Fluent-Bit instead ``` +### Tempo + +**Chart**: `grafana/tempo` +**Version**: 1.24.4 +**Namespace**: `monitoring` + +**Purpose**: Distributed tracing backend receiving OTLP traces from Traefik and other instrumented services. + +**Configuration**: +```yaml +tempo: + storage: + trace: + backend: local + local: + path: /var/tempo/traces + receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +persistence: + enabled: true + size: 10Gi +``` + +**Endpoints**: +- gRPC OTLP receiver: `:4317` +- HTTP OTLP receiver: `:4318` +- Query API: `:3200` + +**Grafana Integration**: +- Trace-to-logs correlation with Loki (by namespace, pod, container) +- Trace-to-metrics correlation with Prometheus (by service name) +- Service graph and node graph visualization + ### Fluent-Bit **Chart**: `fluent/fluent-bit` @@ -728,6 +780,263 @@ promtail: **Output**: Loki +### Gitea + +**Chart**: `gitea/gitea` +**Version**: 12.5.0 (app v1.25.4) +**Namespace**: `gitea` + +**Purpose**: Self-hosted Git repository hosting with pull requests, issues, CI/CD (Gitea Actions), container registry, and package registry. + +**Configuration**: +```yaml +# infra/gitea.yaml + infra/values/gitea-values.yaml +ingress: + host: git.forteapps.net + tls: cert-manager (letsencrypt-prod) + +gitea: + admin: + existingSecret: gitea-credentials + config: + service: + DISABLE_REGISTRATION: true + ALLOW_ONLY_EXTERNAL_REGISTRATION: true + actions: + ENABLED: true + packages: + ENABLED: true + metrics: + ENABLED: true + +postgresql: + enabled: true + persistence: 8Gi (upcloud-block-storage-maxiops) +``` + +**Authentication**: Keycloak OIDC via `forte` realm (client ID: `gitea`). Protocol mapper: `email_verified` hardcoded claim (`true`, boolean) on ID token, Access token, and Userinfo. + +**Endpoints**: +- Web UI: `https://git.forteapps.net` +- SSH: port 22 (ClusterIP) +- Metrics: `/metrics` (Prometheus scrape) + +**Secrets**: `gitea-credentials` (SealedSecret) containing `admin-password`, `postgres-password`, `secret` (OIDC client secret) + +### Gitea Actions Runners + +**Chart**: `actions` (from `https://dl.gitea.com/charts`) +**Namespace**: `gitea` +**Sync Wave**: 2 (deploys after Gitea) + +**Purpose**: Act runners execute Gitea Actions CI/CD workflows. Deployed as a StatefulSet with a Docker-in-Docker sidecar for container-based job execution. + +**Configuration**: +```yaml +# infra/gitea-actions.yaml + infra/values/gitea-actions-values.yaml +replicaCount: 3 + +runner: + labels: + - "ubuntu-latest:docker://node:20-bookworm" + - "ubuntu-22.04:docker://node:20-bookworm" + existingSecret: gitea-runner-token + +gitea: + instance: + url: http://gitea-http.gitea.svc.cluster.local:3000 + +dind: + enabled: true # Docker-in-Docker sidecar (privileged) +``` + +**Resources**: + +| Container | CPU Request | Memory Request | CPU Limit | Memory Limit | +|-----------|-------------|----------------|-----------|--------------| +| Runner | 250m | 256Mi | 1 | 1Gi | +| DinD sidecar | 250m | 256Mi | 1 | 1Gi | + +**Secrets**: `gitea-runner-token` (SealedSecret) containing `token` (instance-level runner registration token from `/admin/runners`) + +**Setup Steps**: +1. Get runner registration token from Gitea admin panel (`/admin/runners`) +2. Fill in `private/gitea-runner-token.yaml` with the token +3. Seal: `kubeseal --format yaml < private/gitea-runner-token.yaml > secrets/gitea-runner-token-sealed.yaml` +4. Commit and push — ArgoCD deploys runners automatically + +**Verification**: +- `kubectl get statefulset -n gitea` — 3/3 runners ready +- Gitea admin panel (`/admin/runners`) — runners show as Online +- Create test workflow in `.gitea/workflows/test.yml` — job executes + +### Keycloak Client Registrar + +**Type**: CronJob (deployed via Keycloak Helm chart `extraDeploy`) +**Namespace**: `keycloak` +**Schedule**: `*/2 * * * *` (every 2 minutes) + +**Purpose**: Handles two responsibilities: +1. **Legacy sync** — extracts secrets from Keycloak clients with `k8s.secret.sync: "true"` attribute (same as former PostSync syncer) +2. **Self-service registration** — processes config Secrets (cloned by Kyverno) to register new OIDC clients and sync their credentials + +**How It Works**: + +*Legacy path (existing clients like Gitea):* +1. Authenticates to Keycloak Admin API using admin credentials from `keycloak-credentials` secret +2. Queries all clients in the `forte` realm +3. Filters clients with `k8s.secret.sync: "true"` attribute +4. For each matching client, retrieves the auto-generated secret via Keycloak Admin API +5. Creates/updates a K8s Secret in the target namespace (from `k8s.secret.namespace` attribute) +6. Always writes a central copy to the `secrets` namespace + +*Self-service path (new clients):* +1. Lists Secrets in `keycloak` namespace with label `keycloak.forteapps.net/client-config=true` +2. For each config Secret, parses `client.json` and computes a config hash +3. Skips if hash matches annotation and credential Secret already exists +4. Creates or updates the Keycloak client via Admin API +5. Fetches the generated client secret +6. Upserts credential Secret in target namespace + central `secrets` namespace +7. Annotates config Secret with sync status, config hash, and timestamp + +**Resources**: +- `ServiceAccount`: `keycloak-client-registrar` (namespace: `keycloak`) +- `ClusterRole`: `keycloak-client-registrar` (secrets: get/list/create/update/patch; namespaces: get/list) +- `ClusterRoleBinding`: `keycloak-client-registrar` +- `CronJob`: `keycloak-client-registrar` + +**Kyverno Policy**: `keycloak-client-config-cloner` — clones labeled Secrets from app namespaces to `keycloak` namespace (see [Kyverno Policies](#kyverno-policies)) + +**Legacy Client Attributes** (set in `forte-realm.json`): + +| Attribute | Required | Default | Description | +|-----------|----------|---------|-------------| +| `k8s.secret.sync` | Yes | — | Set to `"true"` to enable syncing | +| `k8s.secret.namespace` | Yes | — | Target K8s namespace | +| `k8s.secret.name` | Yes | — | Name of the K8s Secret | +| `k8s.secret.client-id-key` | No | `client-id` | Field name for client ID in the Secret | +| `k8s.secret.client-secret-key` | No | `client-secret` | Field name for client secret in the Secret | + +**Self-Service Config Secret Schema**: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-client- + namespace: + labels: + keycloak.forteapps.net/client-config: "true" +stringData: + client.json: | + { + "clientId": "", + "name": "", + "redirectUris": ["https://.forteapps.net/*"], + "webOrigins": ["https://.forteapps.net"], + "defaultClientScopes": ["openid", "email", "profile"], + "protocolMappers": [], + "secret": { + "namespace": "", + "name": "-oidc-credentials", + "keys": { "clientId": "client-id", "clientSecret": "client-secret" } + } + } +``` + +**Created Credential Secret Format**: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: + namespace: + labels: + app.kubernetes.io/managed-by: keycloak-client-registrar +type: Opaque +data: + : + : +``` + +**Config Secret Annotations** (set by registrar): + +| Annotation | Description | +|-----------|-------------| +| `keycloak.forteapps.net/config-hash` | SHA-256 hash of client.json for change detection | +| `keycloak.forteapps.net/sync-status` | `synced` or `error` | +| `keycloak.forteapps.net/last-sync` | ISO 8601 timestamp of last successful sync | + +**Verification**: +```bash +# Check CronJob status +kubectl get cronjobs -n keycloak + +# View latest registrar logs +kubectl logs -n keycloak job/$(kubectl get jobs -n keycloak --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}') + +# Verify created secret +kubectl get secret -n -o yaml + +# Check config Secret annotations (self-service) +kubectl get secret keycloak-client- -n keycloak -o jsonpath='{.metadata.annotations}' +``` + +**See**: [Developer Guide - Adding a New Keycloak Client](DEVELOPER-GUIDE.md#adding-a-new-keycloak-client) + +### Renovate + +**Chart**: `renovate` (OCI: `ghcr.io/renovatebot/charts`) +**Version**: 46.109.0 (app v43.113.0) +**Namespace**: `renovate` +**Sync Wave**: 2 + +**Purpose**: Automated dependency update bot. Runs as a CronJob that scans Gitea repositories for outdated dependencies and creates pull requests with updates. + +**Configuration**: +```yaml +# infra/renovate.yaml + infra/values/renovate-values.yaml +cronjob: + schedule: "@daily" + concurrencyPolicy: Forbid + +renovate: + config: + platform: gitea + endpoint: https://git.forteapps.net + autodiscover: true + gitAuthor: "Renovate Bot " + packageRules: + - matchRepositories: ["**/10x"] + assignees: ["edvard.unsvag"] + reviewers: ["edvard.unsvag"] + - matchRepositories: ["**/auth-sidecar"] + assignees: ["danijel.simeunovic"] + reviewers: ["danijel.simeunovic"] + - matchRepositories: ["**/forte-helm"] + assignees: ["danijel.simeunovic"] + reviewers: ["danijel.simeunovic"] + +resources: + requests: { cpu: 500m, memory: 1Gi } + limits: { cpu: "2", memory: 4Gi } +``` + +**Note**: Assignees and reviewers are only applied at PR creation time. Existing PRs must be closed and recreated for new assignment rules to take effect. + +**Secrets**: `renovate-env` (SealedSecret in `secrets` namespace, cloned by Kyverno) containing: +- `RENOVATE_TOKEN` — Gitea PAT with repo write + issue write permissions +- `RENOVATE_GITHUB_COM_TOKEN` — GitHub PAT (public_repo read-only) for changelog fetching + +**Setup Steps**: +1. Fill in `private/renovate-env.yaml` with tokens +2. Seal: `kubeseal --format yaml < private/renovate-env.yaml > secrets/renovate-env-sealed.yaml` +3. Commit and push — ArgoCD deploys the CronJob, Kyverno clones the secret + +**Verification**: +- `kubectl get cronjob -n renovate` — CronJob exists +- `kubectl create job --from=cronjob/renovate renovate-test -n renovate` — manual trigger +- `kubectl logs -n renovate job/renovate-test` — check logs + --- ## Kyverno Policies @@ -764,6 +1073,19 @@ spec: **Label Requirement**: Secrets must have `allowedToBeCloned: "true"` +### Keycloak Client Config Cloner + +**File**: `cluster-resources/policies/keycloak-client-cloner.yaml` + +**Purpose**: Clones Secrets labeled `keycloak.forteapps.net/client-config: "true"` from app namespaces to the `keycloak` namespace. This allows apps to declare their OIDC client configuration in their own namespace, which the [Keycloak Client Registrar](#keycloak-client-registrar) then processes. + +**Trigger**: Any Secret with label `keycloak.forteapps.net/client-config: "true"` created outside the `keycloak` namespace. + +**Behavior**: +- Generates a copy of the Secret in the `keycloak` namespace with the same name +- Adds source tracking annotations (`keycloak.forteapps.net/source-namespace`, `keycloak.forteapps.net/source-name`) +- `synchronize: true` — changes to the source Secret are reflected in the clone + ### Default Namespace Blocker **File**: `cluster-resources/policies/default-ns-blocker.yaml` @@ -853,7 +1175,7 @@ policies.forteapps.io/auth-token-secret-name: "auth-tokens" policies.forteapps.io/auth-upstream-url: "http://localhost:3000" # Optional customization -policies.forteapps.io/auth-image: "ghcr.io/snothub/stunning-memory" +policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar" policies.forteapps.io/auth-image-version: "latest" ``` @@ -869,7 +1191,7 @@ policies.forteapps.io/auth-oidc-client-id: "myapp" policies.forteapps.io/auth-oidc-callback-path: "/auth/callback" policies.forteapps.io/auth-oidc-scopes: "openid,profile,email" policies.forteapps.io/auth-upstream-url: "http://localhost:3000" -policies.forteapps.io/auth-image: "ghcr.io/snothub/stunning-memory" +policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar" policies.forteapps.io/auth-image-version: "latest" ``` @@ -885,7 +1207,7 @@ policies.forteapps.io/auth-mcp-authority: "https://auth.example.com" policies.forteapps.io/auth-mcp-scopes: "read,write" policies.forteapps.io/auth-upstream-url: "http://localhost:3000" policies.forteapps.io/auth-log-level: "info" -policies.forteapps.io/auth-image: "ghcr.io/snothub/stunning-memory" +policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar" policies.forteapps.io/auth-image-version: "latest" ``` @@ -894,7 +1216,7 @@ policies.forteapps.io/auth-image-version: "latest" **Token Mode**: ```yaml name: authn -image: ghcr.io/snothub/stunning-memory:latest +image: ghcr.io/fortedigital/auth-sidecar:latest ports: - containerPort: 8080 name: auth @@ -929,7 +1251,7 @@ securityContext: **OIDC Mode**: ```yaml name: authn -image: ghcr.io/snothub/stunning-memory:latest +image: ghcr.io/fortedigital/auth-sidecar:latest ports: - containerPort: 8080 name: auth @@ -976,7 +1298,7 @@ securityContext: **MCP Mode**: ```yaml name: authn -image: ghcr.io/snothub/stunning-memory:latest +image: ghcr.io/fortedigital/auth-sidecar:latest ports: - containerPort: 8080 name: auth @@ -1185,6 +1507,19 @@ GET /api/v1/query_range?query={promql}&start={time}&end={time}&step={duration} GET /api/v1/label/__name__/values ``` +### Tempo API + +``` +# Search traces +GET /api/search?q={traceql} + +# Get trace by ID +GET /api/traces/{traceID} + +# Service tag values +GET /api/v2/search/tag/resource.service.name/values +``` + ### Loki API ``` @@ -1316,7 +1651,11 @@ team: platform | **Prometheus** | 2.47.0+ | Latest | | **Grafana** | 10.0.0+ | Latest | | **Loki** | 2.9.0+ | Latest | +| **Tempo** | 2.6.0+ | 1.24.4 | | **Fluent-Bit** | 2.1.0+ | Latest | +| **Gitea** | 1.25.4 | 12.5.0 | +| **Gitea Act Runner** | Latest | Latest | +| **Renovate** | v43.113.0 | 46.109.0 | | **PostgreSQL** | 16-alpine | N/A | | **Trivy** | Latest | Latest | @@ -1328,6 +1667,6 @@ team: platform --- -**Last Updated**: 2026-03-16 +**Last Updated**: 2026-04-16 **Maintained By**: Platform Team **Version**: 1.0.0 diff --git a/infra/base/cert-manager-application.yaml b/infra/base/cert-manager-application.yaml index 8580416..3adddf6 100644 --- a/infra/base/cert-manager-application.yaml +++ b/infra/base/cert-manager-application.yaml @@ -48,10 +48,10 @@ spec: resources: requests: cpu: 50m - memory: 64Mi + memory: 128Mi limits: cpu: 100m - memory: 128Mi + memory: 256Mi # Service account serviceAccount: diff --git a/infra/base/cluster-resources-application.yaml b/infra/base/cluster-resources-application.yaml index 4072f08..6c6ce01 100644 --- a/infra/base/cluster-resources-application.yaml +++ b/infra/base/cluster-resources-application.yaml @@ -15,9 +15,11 @@ spec: project: default source: - repoURL: git@github.com:fortedigital/sturdy-adventure.git + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD path: cluster-resources + directory: + exclude: 'network' destination: server: https://kubernetes.default.svc diff --git a/infra/base/enterprise-apps.yaml b/infra/base/enterprise-apps.yaml index 5d0b5c3..d843b39 100644 --- a/infra/base/enterprise-apps.yaml +++ b/infra/base/enterprise-apps.yaml @@ -16,7 +16,7 @@ metadata: spec: project: default source: - repoURL: git@github.com:fortedigital/sturdy-adventure.git + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD path: apps/overlays/eu destination: diff --git a/infra/base/fluent-bit.yaml b/infra/base/fluent-bit.yaml index 51cf40e..748a6b4 100644 --- a/infra/base/fluent-bit.yaml +++ b/infra/base/fluent-bit.yaml @@ -23,7 +23,7 @@ spec: valueFiles: - $values/infra/values/base/fluent-bit-values.yaml - - repoURL: git@github.com:fortedigital/sturdy-adventure.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD ref: values diff --git a/infra/base/grafana.yaml b/infra/base/grafana.yaml index 3261109..5f18399 100644 --- a/infra/base/grafana.yaml +++ b/infra/base/grafana.yaml @@ -24,7 +24,7 @@ spec: - $values/infra/values/base/grafana-values.yaml - $values/infra/values/eu/grafana-values.yaml - - repoURL: git@github.com:fortedigital/sturdy-adventure.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD ref: values diff --git a/infra/base/keycloak.yaml b/infra/base/keycloak.yaml index a5f753f..ded2964 100644 --- a/infra/base/keycloak.yaml +++ b/infra/base/keycloak.yaml @@ -24,7 +24,7 @@ spec: - $values/infra/values/base/keycloak-values.yaml - $values/infra/values/eu/keycloak-values.yaml - - repoURL: git@github.com:fortedigital/sturdy-adventure.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD ref: values @@ -41,3 +41,9 @@ spec: - CreateNamespace=true - Validate=true - ServerSideApply=true + + ignoreDifferences: + - group: batch + kind: CronJob + jsonPointers: + - /spec/jobTemplate/spec/template/spec/containers/0/args diff --git a/infra/base/kyverno-policies.yaml b/infra/base/kyverno-policies.yaml index 028813a..e00e063 100644 --- a/infra/base/kyverno-policies.yaml +++ b/infra/base/kyverno-policies.yaml @@ -15,7 +15,7 @@ spec: project: default source: - repoURL: git@github.com:fortedigital/sturdy-adventure.git + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD path: cluster-resources/policies diff --git a/infra/base/kyverno.yaml b/infra/base/kyverno.yaml index 77e3ac8..ebc2aa5 100644 --- a/infra/base/kyverno.yaml +++ b/infra/base/kyverno.yaml @@ -39,6 +39,10 @@ spec: targetRevision: v3.7.0 # Update to latest stable version helm: releaseName: kyverno + valuesObject: + grafana: + enabled: true + namespace: monitoring destination: server: https://kubernetes.default.svc diff --git a/infra/base/loki.yaml b/infra/base/loki.yaml index 9d90348..d9acdab 100644 --- a/infra/base/loki.yaml +++ b/infra/base/loki.yaml @@ -23,7 +23,7 @@ spec: valueFiles: - $values/infra/values/base/loki-values.yaml - - repoURL: git@github.com:fortedigital/sturdy-adventure.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD ref: values diff --git a/infra/base/prometheus.yaml b/infra/base/prometheus.yaml index b6f01d0..15aa33d 100644 --- a/infra/base/prometheus.yaml +++ b/infra/base/prometheus.yaml @@ -23,7 +23,7 @@ spec: valueFiles: - $values/infra/values/base/prometheus-values.yaml - - repoURL: git@github.com:fortedigital/sturdy-adventure.git + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git targetRevision: HEAD ref: values diff --git a/infra/base/secrets.yaml b/infra/base/secrets.yaml index 8143603..0e14c6a 100644 --- a/infra/base/secrets.yaml +++ b/infra/base/secrets.yaml @@ -17,7 +17,7 @@ metadata: spec: project: default source: - repoURL: git@github.com:fortedigital/sturdy-adventure.git + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git path: secrets/eu destination: server: https://kubernetes.default.svc diff --git a/infra/dashboards/dot-ai-logs.json b/infra/dashboards/dot-ai-logs.json new file mode 100644 index 0000000..ae23276 --- /dev/null +++ b/infra/dashboards/dot-ai-logs.json @@ -0,0 +1,148 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "title": "Log Volume", + "type": "timeseries", + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 0 + }, + "datasource": { + "type": "loki", + "uid": "loki" + }, + "targets": [ + { + "expr": "sum(count_over_time({namespace=\"dot-ai\"} [1m])) by (pod)", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "bars", + "fillOpacity": 50, + "stacking": { + "mode": "normal" + } + } + }, + "overrides": [] + } + }, + { + "title": "Logs by Pod", + "type": "logs", + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 6 + }, + "datasource": { + "type": "loki", + "uid": "loki" + }, + "targets": [ + { + "expr": "{namespace=\"dot-ai\", pod=~\"$pod\"} | json log=\"log\", message=\"message\", msg=\"msg\", level=\"level\", stream=\"stream\" | label_format level=`{{if .level}}{{.level}}{{else if eq .stream \"stderr\"}}error{{else}}info{{end}}` | line_format `{{.pod}} |{{or .message .msg .log}}`", + "refId": "A" + } + ], + "options": { + "showTime": true, + "showLabels": false, + "showCommonLabels": false, + "wrapLogMessage": true, + "prettifyLogMessage": false, + "enableLogDetails": true, + "sortOrder": "Descending", + "dedupStrategy": "none", + "displayedFields": [ + "pod", + "level" + ] + } + }, + { + "title": "Errors & Warnings", + "type": "logs", + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 22 + }, + "datasource": { + "type": "loki", + "uid": "loki" + }, + "targets": [ + { + "expr": "{namespace=\"dot-ai\", pod=~\"$pod\"} | json log=\"log\", message=\"message\", msg=\"msg\", level=\"level\", stream=\"stream\" | label_format level=`{{if .level}}{{.level}}{{else if eq .stream \"stderr\"}}error{{else}}info{{end}}` | level=~`error|warn|warning|fatal|panic` | line_format `{{.pod}} |{{or .message .msg .log}}`", + "refId": "A" + } + ], + "options": { + "showTime": true, + "showLabels": false, + "showCommonLabels": false, + "wrapLogMessage": true, + "prettifyLogMessage": false, + "enableLogDetails": true, + "sortOrder": "Descending", + "dedupStrategy": "none", + "displayedFields": [ + "pod", + "level" + ] + } + } + ], + "schemaVersion": 39, + "tags": [ + "dot-ai", + "logs", + "loki" + ], + "templating": { + "list": [ + { + "name": "pod", + "type": "query", + "datasource": { + "type": "loki", + "uid": "loki" + }, + "query": { + "label": "pod", + "stream": "{namespace=\"dot-ai\"}", + "type": 1 + }, + "includeAll": true, + "multi": true, + "current": { + "selected": true, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "dot-ai Logs", + "uid": "dot-ai-logs" +} diff --git a/infra/dashboards/kustomization.yaml b/infra/dashboards/kustomization.yaml new file mode 100644 index 0000000..98af3cf --- /dev/null +++ b/infra/dashboards/kustomization.yaml @@ -0,0 +1,25 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: monitoring + +generatorOptions: + disableNameSuffixHash: true + labels: + grafana_dashboard: "1" + +configMapGenerator: +- name: grafana-dashboard-trivy + files: + - trivy.json +- name: grafana-dashboard-traefik-loki + files: + - traefik-loki.json +- name: grafana-dashboard-dot-ai-logs + files: + - dot-ai-logs.json +- name: grafana-dashboard-opencost + files: + - opencost.json +- name: grafana-dashboard-pod-security + files: + - pod-security.json diff --git a/infra/dashboards/opencost.json b/infra/dashboards/opencost.json new file mode 100644 index 0000000..aa34b31 --- /dev/null +++ b/infra/dashboards/opencost.json @@ -0,0 +1,1510 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "UpCloud 4-node cluster cost monitoring powered by OpenCost with custom pricing.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 100, + "title": "Monthly Cost Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Monthly CPU cost based on provisioned capacity and OpenCost custom pricing.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "title": "Monthly CPU Cost", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) by (node) * avg(node_cpu_hourly_cost) by (node) * 730 * (1-$useDiscount/100))", + "legendFormat": "CPU Cost", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Monthly memory cost based on provisioned capacity and OpenCost custom pricing.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "title": "Monthly Memory Cost", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) by (node) / 1024/1024/1024 * avg(node_ram_hourly_cost) by (node) * 730 * (1-$useDiscount/100))", + "legendFormat": "Memory Cost", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Monthly storage cost from PV hourly costs and local disk at $localStorageGBCost/GB.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "title": "Monthly Storage Cost", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(pv_hourly_cost) by (persistentvolume) * 730 * avg(kube_persistentvolume_capacity_bytes) by (persistentvolume) / 1024/1024/1024) + (sum(sum(container_fs_limit_bytes{device!=\"tmpfs\", id=\"/\"}) by (instance) / 1024/1024/1024) * $localStorageGBCost OR on() vector(0))", + "legendFormat": "Storage Cost", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total monthly infrastructure cost (CPU + Memory + Storage).", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "title": "Total Monthly Cost", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) by (node) * avg(node_cpu_hourly_cost) by (node) * 730 * (1-$useDiscount/100)) + sum(avg(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) by (node) / 1024/1024/1024 * avg(node_ram_hourly_cost) by (node) * 730 * (1-$useDiscount/100)) + sum(avg(pv_hourly_cost) by (persistentvolume) * 730 * avg(kube_persistentvolume_capacity_bytes) by (persistentvolume) / 1024/1024/1024) + (sum(sum(container_fs_limit_bytes{device!=\"tmpfs\", id=\"/\"}) by (instance) / 1024/1024/1024) * $localStorageGBCost OR on() vector(0))", + "legendFormat": "Total Cost", + "refId": "A" + } + ] + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 101, + "title": "Resource Utilization", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of total CPU capacity currently in use.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 6 + }, + "id": 5, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "title": "CPU Utilization", + "type": "gauge", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(rate(container_cpu_usage_seconds_total{image!=\"\"}[5m])) / sum(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) * 100", + "legendFormat": "CPU Utilization", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of total CPU capacity reserved by resource requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 6 + }, + "id": 6, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "title": "CPU Requests", + "type": "gauge", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_resource_requests{resource=\"cpu\", unit=\"core\"}) / sum(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) * 100", + "legendFormat": "CPU Requests", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of total memory capacity currently in use.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 6 + }, + "id": 7, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "title": "RAM Utilization", + "type": "gauge", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(container_memory_working_set_bytes{image!=\"\"}) / sum(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) * 100", + "legendFormat": "RAM Utilization", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of total memory capacity reserved by resource requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 6 + }, + "id": 8, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "title": "RAM Requests", + "type": "gauge", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_resource_requests{resource=\"memory\", unit=\"byte\"}) / sum(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) * 100", + "legendFormat": "RAM Requests", + "refId": "A" + } + ] + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 102, + "title": "Cost Trends", + "type": "row", + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Hourly cost trend stacked by resource type.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "normal" + } + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 9, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "Total Cost Over Time", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) by (node) * avg(node_cpu_hourly_cost) by (node))", + "legendFormat": "CPU", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) by (node) / 1024/1024/1024 * avg(node_ram_hourly_cost) by (node))", + "legendFormat": "Memory", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(pv_hourly_cost) by (persistentvolume) * avg(kube_persistentvolume_capacity_bytes) by (persistentvolume) / 1024/1024/1024) + (sum(sum(container_fs_limit_bytes{device!=\"tmpfs\", id=\"/\"}) by (instance) / 1024/1024/1024) * $localStorageGBCost / 730 OR on() vector(0))", + "legendFormat": "Storage", + "refId": "C" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Hourly cost trend broken down by namespace.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "normal" + } + }, + "decimals": 4, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "Cost by Namespace Over Time", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(container_cpu_allocation{namespace=~\"$namespace\"} * on(node) group_left() node_cpu_hourly_cost + container_memory_allocation_bytes{namespace=~\"$namespace\"} / 1024/1024/1024 * on(node) group_left() node_ram_hourly_cost) by (namespace)", + "legendFormat": "{{namespace}}", + "refId": "A" + } + ] + } + ] + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 103, + "title": "Cost Breakdown", + "type": "row", + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Monthly cost breakdown per node showing CPU and memory costs.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Node" + }, + "properties": [ + { + "id": "custom.width", + "value": 250 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 11, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "CPU Cost" + } + ] + }, + "title": "Monthly Cost by Node", + "type": "table", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "node" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 2": true + }, + "renameByName": { + "Value #A": "CPU Cost", + "Value #B": "Memory Cost", + "node": "Node" + } + } + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "avg(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) by (node) * avg(node_cpu_hourly_cost) by (node) * 730", + "format": "table", + "instant": true, + "legendFormat": "CPU Cost", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "avg(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) by (node) / 1024/1024/1024 * avg(node_ram_hourly_cost) by (node) * 730", + "format": "table", + "instant": true, + "legendFormat": "Memory Cost", + "refId": "B" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Monthly cost split by resource type.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 12, + "options": { + "displayMode": "gradient", + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "valueMode": "color" + }, + "title": "Monthly Cost by Resource", + "type": "bargauge", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"cpu\", unit=\"core\"}) by (node) * avg(node_cpu_hourly_cost) by (node) * 730)", + "legendFormat": "CPU", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(kube_node_status_capacity{resource=\"memory\", unit=\"byte\"}) by (node) / 1024/1024/1024 * avg(node_ram_hourly_cost) by (node) * 730)", + "legendFormat": "Memory", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(avg(pv_hourly_cost) by (persistentvolume) * 730 * avg(kube_persistentvolume_capacity_bytes) by (persistentvolume) / 1024/1024/1024) + (sum(sum(container_fs_limit_bytes{device!=\"tmpfs\", id=\"/\"}) by (instance) / 1024/1024/1024) * $localStorageGBCost OR on() vector(0))", + "legendFormat": "Storage", + "refId": "C" + } + ] + } + ] + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 104, + "title": "Namespace & Container Costs", + "type": "row", + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Hourly cost distribution across namespaces.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 4, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 13, + "options": { + "displayLabels": [ + "name", + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "value", + "percent" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Hourly Cost by Namespace", + "type": "piechart", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(container_cpu_allocation * on(node) group_left() node_cpu_hourly_cost + container_memory_allocation_bytes / 1024/1024/1024 * on(node) group_left() node_ram_hourly_cost) by (namespace)", + "legendFormat": "{{namespace}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Hourly cost distribution across containers in the selected namespace(s).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 4, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 14, + "options": { + "displayLabels": [ + "name", + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "value", + "percent" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Hourly Cost by Container", + "type": "piechart", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(container_cpu_allocation{namespace=~\"$namespace\"} * on(node) group_left() node_cpu_hourly_cost + container_memory_allocation_bytes{namespace=~\"$namespace\"} / 1024/1024/1024 * on(node) group_left() node_ram_hourly_cost) by (container)", + "legendFormat": "{{container}}", + "refId": "A" + } + ] + } + ] + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 105, + "title": "Cost Efficiency", + "type": "row", + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Compare CPU requests against actual usage to identify over-provisioning.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisLabel": "CPU Cores", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Requested/" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 15, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "CPU Request vs Actual Usage by Namespace", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_resource_requests{resource=\"cpu\", unit=\"core\", namespace=~\"$namespace\"}) by (namespace)", + "legendFormat": "{{namespace}} - Requested", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\", image!=\"\"}[5m])) by (namespace)", + "legendFormat": "{{namespace}} - Actual", + "refId": "B" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Compare memory requests against actual usage to identify over-provisioning.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisLabel": "Memory", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Requested/" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 16, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "Memory Request vs Actual Usage by Namespace", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_resource_requests{resource=\"memory\", unit=\"byte\", namespace=~\"$namespace\"}) by (namespace)", + "legendFormat": "{{namespace}} - Requested", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(container_memory_working_set_bytes{namespace=~\"$namespace\", image!=\"\"}) by (namespace)", + "legendFormat": "{{namespace}} - Actual", + "refId": "B" + } + ] + } + ] + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": [ + "cost", + "opencost", + "upcloud" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(namespace)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "0", + "value": "0" + }, + "hide": 0, + "label": "Sustained Use Discount %", + "name": "useDiscount", + "options": [ + { + "selected": true, + "text": "0", + "value": "0" + } + ], + "query": "0", + "skipUrlSync": false, + "type": "textbox" + }, + { + "current": { + "selected": false, + "text": "0.03", + "value": "0.03" + }, + "hide": 2, + "label": "Local Storage GB Cost", + "name": "localStorageGBCost", + "options": [ + { + "selected": true, + "text": "0.03", + "value": "0.03" + } + ], + "query": "0.03", + "skipUrlSync": false, + "type": "constant" + } + ] + }, + "time": { + "from": "now-7d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Cluster Cost Overview", + "uid": "JOUdHGZZz", + "version": 1, + "weekStart": "" +} diff --git a/infra/dashboards/pod-security.json b/infra/dashboards/pod-security.json new file mode 100644 index 0000000..8e8fd4e --- /dev/null +++ b/infra/dashboards/pod-security.json @@ -0,0 +1,399 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "title": "Enforced Denials", + "description": "Pods rejected by Pod Security Standards (enforce mode)", + "type": "stat", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 }, + "targets": [ + { + "expr": "sum(increase(pod_security_evaluations_total{decision=\"deny\", mode=\"enforce\"}[$__range])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": null, "color": "green" }, + { "value": 1, "color": "red" } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"] }, + "colorMode": "background", + "textMode": "auto" + } + }, + { + "title": "Audit Violations", + "description": "Pods that violate audit-level policy (allowed but logged)", + "type": "stat", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 5, "w": 6, "x": 6, "y": 0 }, + "targets": [ + { + "expr": "sum(increase(pod_security_evaluations_total{decision=\"deny\", mode=\"audit\"}[$__range])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": null, "color": "green" }, + { "value": 1, "color": "orange" } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"] }, + "colorMode": "background", + "textMode": "auto" + } + }, + { + "title": "Warnings", + "description": "Pods that triggered warn-level policy (allowed with warning)", + "type": "stat", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 5, "w": 6, "x": 12, "y": 0 }, + "targets": [ + { + "expr": "sum(increase(pod_security_evaluations_total{decision=\"deny\", mode=\"warn\"}[$__range])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": null, "color": "green" }, + { "value": 1, "color": "yellow" } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"] }, + "colorMode": "background", + "textMode": "auto" + } + }, + { + "title": "Total Evaluations", + "description": "All pod security evaluations across all modes", + "type": "stat", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 5, "w": 6, "x": 18, "y": 0 }, + "targets": [ + { + "expr": "sum(increase(pod_security_evaluations_total[$__range])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": null, "color": "blue" } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"] }, + "colorMode": "background", + "textMode": "auto" + } + }, + { + "title": "Violation Rate by Mode", + "description": "Rate of policy violations over time, grouped by enforcement mode", + "type": "timeseries", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 }, + "targets": [ + { + "expr": "sum(rate(pod_security_evaluations_total{decision=\"deny\", mode=\"enforce\"}[5m]))", + "legendFormat": "enforce (denied)", + "refId": "A" + }, + { + "expr": "sum(rate(pod_security_evaluations_total{decision=\"deny\", mode=\"audit\"}[5m]))", + "legendFormat": "audit", + "refId": "B" + }, + { + "expr": "sum(rate(pod_security_evaluations_total{decision=\"deny\", mode=\"warn\"}[5m]))", + "legendFormat": "warn", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 15, + "pointSize": 5, + "showPoints": "auto" + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "enforce (denied)" }, + "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] + }, + { + "matcher": { "id": "byName", "options": "audit" }, + "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] + }, + { + "matcher": { "id": "byName", "options": "warn" }, + "properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }] + } + ] + } + }, + { + "title": "Violations by Policy Level", + "description": "Violation rate grouped by the PSS level that was violated", + "type": "timeseries", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 }, + "targets": [ + { + "expr": "sum(rate(pod_security_evaluations_total{decision=\"deny\"}[5m])) by (policy_level)", + "legendFormat": "{{ policy_level }}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 15, + "pointSize": 5, + "showPoints": "auto" + }, + "unit": "ops" + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "restricted" }, + "properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }] + }, + { + "matcher": { "id": "byName", "options": "baseline" }, + "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] + }, + { + "matcher": { "id": "byName", "options": "privileged" }, + "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] + } + ] + } + }, + { + "title": "Enforced Denials by Namespace", + "description": "Pods blocked per namespace (enforce mode only)", + "type": "timeseries", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 }, + "targets": [ + { + "expr": "sum(rate(pod_security_evaluations_total{decision=\"deny\", mode=\"enforce\"}[5m])) by (resource_namespace)", + "legendFormat": "{{ resource_namespace }}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "bars", + "lineWidth": 1, + "fillOpacity": 80, + "stacking": { "mode": "normal" } + }, + "unit": "ops" + }, + "overrides": [] + } + }, + { + "title": "Audit + Warn Violations by Namespace", + "description": "Non-enforced violations per namespace — candidates for tightening", + "type": "timeseries", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 }, + "targets": [ + { + "expr": "sum(rate(pod_security_evaluations_total{decision=\"deny\", mode=~\"audit|warn\"}[5m])) by (resource_namespace)", + "legendFormat": "{{ resource_namespace }}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "bars", + "lineWidth": 1, + "fillOpacity": 80, + "stacking": { "mode": "normal" } + }, + "unit": "ops" + }, + "overrides": [] + } + }, + { + "title": "Violations Breakdown", + "description": "Detailed breakdown of all policy violations", + "type": "table", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 21 }, + "targets": [ + { + "expr": "sum(increase(pod_security_evaluations_total{decision=\"deny\"}[$__range])) by (resource_namespace, policy_level, mode, request_operation) > 0", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { "Time": true }, + "renameByName": { + "resource_namespace": "Namespace", + "policy_level": "Policy Level", + "mode": "Mode", + "request_operation": "Operation", + "Value": "Violations" + }, + "indexByName": { + "resource_namespace": 0, + "policy_level": 1, + "mode": 2, + "request_operation": 3, + "Value": 4 + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { "field": "Violations", "desc": true } + ] + } + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { "id": "byName", "options": "Mode" }, + "properties": [ + { + "id": "mappings", + "value": [ + { "type": "value", "options": { "enforce": { "text": "Enforce", "color": "red" }, "audit": { "text": "Audit", "color": "orange" }, "warn": { "text": "Warn", "color": "yellow" } } } + ] + } + ] + }, + { + "matcher": { "id": "byName", "options": "Violations" }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { "type": "color-background", "mode": "gradient" } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { "value": null, "color": "transparent" }, + { "value": 1, "color": "orange" }, + { "value": 100, "color": "red" } + ] + } + } + ] + } + ] + } + }, + { + "title": "Exemptions", + "description": "Pods exempted from policy evaluation", + "type": "timeseries", + "datasource": { "type": "prometheus" }, + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 31 }, + "targets": [ + { + "expr": "sum(rate(pod_security_exemptions_total[5m])) by (request_namespace)", + "legendFormat": "{{ request_namespace }}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 10 + }, + "unit": "ops" + }, + "overrides": [] + } + } + ], + "schemaVersion": 39, + "tags": [ + "security", + "pod-security", + "pss", + "compliance" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "title": "Pod Security Violations", + "uid": "pod-security-violations" +} diff --git a/infra/dashboards/traefik-loki.json b/infra/dashboards/traefik-loki.json new file mode 100644 index 0000000..113ddfe --- /dev/null +++ b/infra/dashboards/traefik-loki.json @@ -0,0 +1,1192 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Loki version 2 showcase using JSON Traefik access logs.", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 13713, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "gridPos": { + "h": 2, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 32, + "interval": "", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "
\n \n Traefik Dashboard\n
", + "mode": "html" + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "loki_build_info", + "format": "table", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "transparent": true, + "type": "text" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 2 + }, + "id": 4, + "interval": "1m", + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum(count_over_time({namespace=\"traefik-system\"} |= \"RequestProtocol\" [$__interval]))", + "legendFormat": "", + "queryType": "range", + "refId": "A" + } + ], + "title": "Total requests ", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 6, + "y": 2 + }, + "id": 5, + "interval": "30s", + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum by (OriginStatus) (count_over_time({namespace=\"traefik-system\"}|= \"RequestProtocol\" | json | __error__=\"\" [$__interval]))", + "legendFormat": "HTTP Status: {{OriginStatus}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Requests per status code", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "max": 100, + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 15, + "y": 2 + }, + "id": 19, + "interval": "5m", + "maxDataPoints": 1, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": " sum(rate({namespace=\"traefik-system\"} |~ \"RequestProtocol\" | json | OriginStatus >= 400 |__error__=\"\"[$__interval])) / (sum(rate({namespace=\"traefik-system\"} |~ \"RequestProtocol\" | json | __error__=\"\"[$__interval])) / 100)", + "legendFormat": "", + "refId": "A" + } + ], + "title": "% of 4/5xx ", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "hidden" + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 19, + "y": 2 + }, + "id": 36, + "interval": "1m", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "maxHeight": 600, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "7.3.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": " sum by (OriginStatus,ServiceName) (count_over_time({namespace=\"traefik-system\"} |~ \"RequestProtocol\" | json | OriginStatus >= 400 |__error__=\"\"[$__interval]))", + "legendFormat": " {{ServiceName}} / {{OriginStatus}} ", + "refId": "A" + } + ], + "title": " 4/5xx Services", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 6 + }, + "id": 22, + "interval": "5m", + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "count(sum by (ClientHost) (count_over_time({namespace=\"traefik-system\"}|= \"RequestProtocol\" | json | __error__=\"\" [$__interval])))", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Users right now", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 9, + "x": 15, + "y": 6 + }, + "id": 8, + "interval": "1m", + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "sum_over_time({namespace=\"traefik-system\"}|= \"RequestProtocol\" | json | OriginStatus=200 | unwrap DownstreamContentSize | __error__=\"\" [$__interval])", + "legendFormat": "Bytes sent", + "refId": "A" + } + ], + "title": "Total Bytes Sent", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "sum" + ] + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Total": "Bytes Sent" + } + } + } + ], + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 10 + }, + "id": 33, + "interval": "5m", + "maxDataPoints": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "value", + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.3.4", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "sum by (RouterName) (count_over_time({namespace=\"traefik-system\"}|= \"RequestProtocol\" | json | __error__=\"\" [$__interval]))", + "legendFormat": "{{RouterName}}", + "refId": "A" + } + ], + "title": "Requests Route", + "transparent": true, + "type": "piechart" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "gridPos": { + "h": 17, + "w": 16, + "x": 8, + "y": 10 + }, + "id": 37, + "options": { + "edges": {}, + "nodes": {} + }, + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "limit": 20, + "queryType": "serviceMap", + "refId": "A", + "tableType": "traces" + } + ], + "title": "Service graph", + "type": "nodeGraph" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 90, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "red", + "value": 0.3 + } + ] + }, + "unit": "ns" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "95th percentile" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max latency" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "super-light-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max latency" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 30 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 18 + }, + "id": 16, + "interval": "30s", + "options": { + "legend": { + "calcs": [ + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "maxHeight": 600, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "7.3.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "quantile_over_time(0.95,{namespace=\"traefik-system\"} |= \"RequestProtocol\"| json | unwrap Duration | __error__=\"\" [$__interval]) by (ServiceName)", + "hide": false, + "legendFormat": " {{ ServiceName }}", + "refId": "C" + } + ], + "title": "95th percentile of Request Time", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 90, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "red", + "value": 0.3 + } + ] + }, + "unit": "ns" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "95th percentile" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max latency" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "super-light-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max latency" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 30 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 27 + }, + "id": 34, + "interval": "30s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "maxHeight": 600, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "7.3.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "max by (ServiceName) (max_over_time({namespace=\"traefik-system\"} |= \"RequestProtocol\" |json | unwrap Duration | __error__=\"\" [$__interval]))", + "hide": false, + "legendFormat": "{{ ServiceName}}", + "refId": "D" + } + ], + "title": "Max Age of Request Time", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "h": 16, + "w": 16, + "x": 8, + "y": 27 + }, + "id": 11, + "interval": "", + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "{namespace=\"traefik-system\"} |= \"RequestProtocol\" | json | line_format \"{{.OriginStatus}} {{.RequestMethod}} {{.RequestAddr}}{{.RequestPath}} -> {{.ServiceAddr}}\"", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Logs", + "transparent": true, + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 90, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "red", + "value": 0.3 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "95th percentile" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max latency" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "super-light-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max latency" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 30 + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 35, + "interval": "30s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "maxHeight": 600, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "7.3.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "expr": "sum by (ServiceName) (sum_over_time({namespace=\"traefik-system\"} |= \"RequestProtocol\" |json | unwrap RequestContentSize | __error__=\"\" [$__interval]))", + "hide": false, + "legendFormat": "{{ ServiceName}}", + "refId": "D" + } + ], + "title": "Requests Size", + "transparent": true, + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timeRangeUpdatedDuringEditOrView": false, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Traefik Via Loki", + "uid": "fJarCeaGk", + "version": 1, + "weekStart": "" +} diff --git a/infra/dashboards/trivy.json b/infra/dashboards/trivy.json new file mode 100644 index 0000000..ddc241d --- /dev/null +++ b/infra/dashboards/trivy.json @@ -0,0 +1,1841 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Monitore Trivy Image Vulnerabilities.\r\n\r\nBased on https://grafana.com/grafana/dashboards/17080-trivy-image-vulnerabilities/ and https://grafana.com/grafana/dashboards/16652-trivy-operator-reports/.", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 17214, + "graphTooltip": 1, + "id": 8, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 43, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Image Vulnerabilities", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "purple", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 52, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{severity=\"Unknown\", namespace=~\"$namespace\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "UNKNOWN", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 1 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{severity=\"Low\", namespace=~\"$namespace\"})", + "format": "time_series", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 1 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{severity=\"Medium\", namespace=~\"$namespace\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 12, + "y": 1 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{severity=\"High\", namespace=~\"$namespace\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 16, + "y": 1 + }, + "id": 51, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{severity=\"Critical\", namespace=~\"$namespace\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "text", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "TOTAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "kube-system" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 4 + }, + "id": 58, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"}) by (namespace)", + "instant": false, + "interval": "", + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + } + ], + "title": "Image Vulnerabilities by namespace", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 61, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "maxHeight": 600, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"}) by (severity)", + "instant": false, + "interval": "", + "legendFormat": "{{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Image Vulnerabilities by severity", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "transparent", + "mode": "fixed" + }, + "custom": { + "align": "left", + "cellOptions": { + "mode": "basic", + "type": "color-background" + }, + "filterable": true, + "inspect": false + }, + "links": [ + { + "targetBlank": true, + "title": "Go to CVE", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=${__data.fields.vuln_id}" + } + ], + "mappings": [ + { + "options": { + "CRITICAL": { + "color": "dark-red", + "index": 0 + }, + "HIGH": { + "color": "yellow", + "index": 1 + }, + "LOW": { + "color": "dark-blue", + "index": 3 + }, + "MEDIUM": { + "color": "dark-orange", + "index": 2 + }, + "UNKNOWN": { + "color": "super-light-blue", + "index": 4 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "namespace" + }, + "properties": [ + { + "id": "custom.width" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "pod" + }, + "properties": [ + { + "id": "custom.width" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 67, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 2, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "severity" + } + ] + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"}) without (namespace, last_modified_date, instance, job, endpoint, service, container, image_digest, resource, resource_name)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Image by CVE", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "instance": true, + "job": true + }, + "indexByName": { + "Time": 10, + "Value": 9, + "image_digest": 7, + "image_registry": 4, + "image_repository": 5, + "image_tag": 6, + "name": 8, + "namespace": 2, + "pod": 3, + "severity": 0, + "vuln_id": 1 + }, + "renameByName": { + "image_digest": "", + "name": "source workload" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }, + "id": 70, + "panels": [], + "title": "Config Audits", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 0, "y": 23 }, + "id": 71, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"Critical\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 6, "y": 23 }, + "id": 72, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"High\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 12, "y": 23 }, + "id": 73, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"Medium\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "blue", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 18, "y": 23 }, + "id": 74, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"Low\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": true, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 26 }, + "id": 75, + "options": { + "legend": { "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, + "tooltip": { "maxHeight": 600, "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "expr": "sum(trivy_resource_configaudits{namespace=~\"$namespace\"}) by (namespace)", + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + }], + "title": "Config Audits by Namespace", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": true, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 26 }, + "id": 76, + "options": { + "legend": { "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, + "tooltip": { "maxHeight": 600, "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "expr": "sum(trivy_resource_configaudits{namespace=~\"$namespace\"}) by (severity)", + "legendFormat": "{{severity}}", + "range": true, + "refId": "A" + }], + "title": "Config Audits by Severity", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "fixedColor": "transparent", "mode": "fixed" }, + "custom": { "align": "left", "cellOptions": { "mode": "basic", "type": "color-background" }, "filterable": true, "inspect": false }, + "links": [], + "mappings": [{ + "options": { + "Critical": { "color": "dark-red", "index": 0 }, + "High": { "color": "yellow", "index": 1 }, + "Medium": { "color": "dark-orange", "index": 2 }, + "Low": { "color": "dark-blue", "index": 3 } + }, + "type": "value" + }], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 34 }, + "id": 77, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true, + "sortBy": [{ "desc": false, "displayName": "severity" }] + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{namespace=~\"$namespace\"}) by (namespace, severity, config_audit_id, config_audit_title)", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "Config Audit Findings", + "transformations": [{ + "id": "organize", + "options": { + "excludeByName": { "Time": true, "Value": true }, + "indexByName": { "severity": 0, "config_audit_id": 1, "config_audit_title": 2, "namespace": 3 }, + "renameByName": { "config_audit_id": "Audit ID", "config_audit_title": "Description" } + } + }], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 44 }, + "id": 80, + "panels": [], + "title": "Exposed Secrets", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 0, "y": 45 }, + "id": 81, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_exposedsecrets{severity=\"Critical\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 6, "y": 45 }, + "id": 82, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_exposedsecrets{severity=\"High\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 12, "y": 45 }, + "id": 83, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_exposedsecrets{severity=\"Medium\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "blue", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 18, "y": 45 }, + "id": 84, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_exposedsecrets{severity=\"Low\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": true, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 48 }, + "id": 85, + "options": { + "legend": { "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, + "tooltip": { "maxHeight": 600, "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "expr": "sum(trivy_image_exposedsecrets{namespace=~\"$namespace\"}) by (namespace)", + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + }], + "title": "Exposed Secrets by Namespace", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": true, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 48 }, + "id": 86, + "options": { + "legend": { "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, + "tooltip": { "maxHeight": 600, "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "expr": "sum(trivy_image_exposedsecrets{namespace=~\"$namespace\"}) by (severity)", + "legendFormat": "{{severity}}", + "range": true, + "refId": "A" + }], + "title": "Exposed Secrets by Severity", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "fixedColor": "transparent", "mode": "fixed" }, + "custom": { "align": "left", "cellOptions": { "mode": "basic", "type": "color-background" }, "filterable": true, "inspect": false }, + "links": [], + "mappings": [{ + "options": { + "Critical": { "color": "dark-red", "index": 0 }, + "High": { "color": "yellow", "index": 1 }, + "Medium": { "color": "dark-orange", "index": 2 }, + "Low": { "color": "dark-blue", "index": 3 } + }, + "type": "value" + }], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 56 }, + "id": 87, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true, + "sortBy": [{ "desc": false, "displayName": "severity" }] + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_exposedsecrets{namespace=~\"$namespace\"}) by (namespace, severity, secret_category, secret_title, image_registry, image_repository, image_tag)", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "Exposed Secrets Detail", + "transformations": [{ + "id": "organize", + "options": { + "excludeByName": { "Time": true, "Value": true }, + "indexByName": { "severity": 0, "secret_category": 1, "secret_title": 2, "namespace": 3, "image_registry": 4, "image_repository": 5, "image_tag": 6 }, + "renameByName": { "secret_category": "Category", "secret_title": "Secret", "image_registry": "Registry", "image_repository": "Repository", "image_tag": "Tag" } + } + }], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 66 }, + "id": 90, + "panels": [], + "title": "RBAC Assessments", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 0, "y": 67 }, + "id": 91, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_rbacassessments{severity=\"Critical\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 6, "y": 67 }, + "id": 92, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_rbacassessments{severity=\"High\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 12, "y": 67 }, + "id": 93, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_rbacassessments{severity=\"Medium\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "blue", "value": 1 }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 6, "x": 18, "y": 67 }, + "id": 94, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_rbacassessments{severity=\"Low\", namespace=~\"$namespace\"}) or vector(0)", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": true, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 70 }, + "id": 95, + "options": { + "legend": { "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, + "tooltip": { "maxHeight": 600, "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "expr": "sum(trivy_resource_rbacassessments{namespace=~\"$namespace\"}) by (namespace)", + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + }], + "title": "RBAC Assessments by Namespace", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": true, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 70 }, + "id": 96, + "options": { + "legend": { "calcs": [], "displayMode": "table", "placement": "right", "showLegend": true }, + "tooltip": { "maxHeight": 600, "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "expr": "sum(trivy_resource_rbacassessments{namespace=~\"$namespace\"}) by (severity)", + "legendFormat": "{{severity}}", + "range": true, + "refId": "A" + }], + "title": "RBAC Assessments by Severity", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "fieldConfig": { + "defaults": { + "color": { "fixedColor": "transparent", "mode": "fixed" }, + "custom": { "align": "left", "cellOptions": { "mode": "basic", "type": "color-background" }, "filterable": true, "inspect": false }, + "links": [], + "mappings": [{ + "options": { + "Critical": { "color": "dark-red", "index": 0 }, + "High": { "color": "yellow", "index": 1 }, + "Medium": { "color": "dark-orange", "index": 2 }, + "Low": { "color": "dark-blue", "index": 3 } + }, + "type": "value" + }], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 78 }, + "id": 97, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true, + "sortBy": [{ "desc": false, "displayName": "severity" }] + }, + "pluginVersion": "11.0.0", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_rbacassessments{namespace=~\"$namespace\"}) by (namespace, severity, rbac_assessment_id, rbac_assessment_title)", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + }], + "title": "RBAC Assessment Findings", + "transformations": [{ + "id": "organize", + "options": { + "excludeByName": { "Time": true, "Value": true }, + "indexByName": { "severity": 0, "rbac_assessment_id": 1, "rbac_assessment_title": 2, "namespace": 3 }, + "renameByName": { "rbac_assessment_id": "Assessment ID", "rbac_assessment_title": "Description" } + } + }], + "type": "table" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "Prometheus", + "Addons", + "Trivy", + "Security" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "kube-system" + ], + "value": [ + "kube-system" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values({job=\"trivy-operator\", __name__=~\"trivy_vulnerability_id|trivy_resource_configaudits|trivy_image_exposedsecrets|trivy_resource_rbacassessments\"}, namespace)", + "hide": 0, + "includeAll": false, + "label": "namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values({job=\"trivy-operator\", __name__=~\"trivy_vulnerability_id|trivy_resource_configaudits|trivy_image_exposedsecrets|trivy_resource_rbacassessments\"}, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timeRangeUpdatedDuringEditOrView": false, + "timepicker": {}, + "timezone": "", + "title": "Trivy Operator / Security Overview", + "uid": "trivy_starboard_operator", + "version": 7, + "weekStart": "" +} diff --git a/infra/gitea-actions.yaml b/infra/gitea-actions.yaml new file mode 100644 index 0000000..1531d69 --- /dev/null +++ b/infra/gitea-actions.yaml @@ -0,0 +1,48 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitea-actions + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "2" + labels: + app.kubernetes.io/name: gitea-actions + app.kubernetes.io/part-of: platform + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: https://dl.gitea.com/charts + chart: actions + targetRevision: "0.0.5" + helm: + releaseName: gitea-actions + valueFiles: + - $values/infra/values/gitea-actions-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: gitea + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/infra/gitea.yaml b/infra/gitea.yaml new file mode 100644 index 0000000..f0c5209 --- /dev/null +++ b/infra/gitea.yaml @@ -0,0 +1,52 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitea + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + labels: + app.kubernetes.io/name: gitea + app.kubernetes.io/part-of: platform + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: https://dl.gitea.com/charts + chart: gitea + targetRevision: "12.5.0" + helm: + releaseName: gitea + valueFiles: + - $values/infra/values/gitea-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: gitea + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates + - group: v1 + kind: Secret + jsonPointers: + - /data/postgres-password diff --git a/infra/grafana-dashboards.yaml b/infra/grafana-dashboards.yaml new file mode 100644 index 0000000..2784c00 --- /dev/null +++ b/infra/grafana-dashboards.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: grafana-dashboards + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "2" + labels: + app.kubernetes.io/name: grafana-dashboards + app.kubernetes.io/part-of: monitoring + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + path: infra/dashboards + + destination: + server: https://kubernetes.default.svc + namespace: monitoring + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true diff --git a/infra/network-policies-application.yaml b/infra/network-policies-application.yaml new file mode 100644 index 0000000..16ae239 --- /dev/null +++ b/infra/network-policies-application.yaml @@ -0,0 +1,33 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: network-policies + namespace: argocd + labels: + app.kubernetes.io/name: network-policies + app.kubernetes.io/part-of: platform + app.kubernetes.io/managed-by: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + path: cluster-resources/network + + destination: + server: https://kubernetes.default.svc + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + + syncOptions: + - Validate=true + - ServerSideApply=true diff --git a/infra/renovate.yaml b/infra/renovate.yaml new file mode 100644 index 0000000..bc1d34e --- /dev/null +++ b/infra/renovate.yaml @@ -0,0 +1,42 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: renovate + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "2" + labels: + app.kubernetes.io/name: renovate + app.kubernetes.io/part-of: platform + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: ghcr.io/renovatebot/charts + chart: renovate + targetRevision: "46.109.0" + helm: + releaseName: renovate + valueFiles: + - $values/infra/values/renovate-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: renovate + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true diff --git a/infra/tempo.yaml b/infra/tempo.yaml new file mode 100644 index 0000000..1ce4fbb --- /dev/null +++ b/infra/tempo.yaml @@ -0,0 +1,42 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: tempo + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + labels: + app.kubernetes.io/name: tempo + app.kubernetes.io/part-of: monitoring + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: https://grafana.github.io/helm-charts + chart: tempo + targetRevision: "1.24.4" + helm: + releaseName: tempo + valueFiles: + - $values/infra/values/tempo-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: monitoring + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true diff --git a/infra/values/base/argocd-values.yaml b/infra/values/base/argocd-values.yaml index f9a65f9..83dc1b3 100644 --- a/infra/values/base/argocd-values.yaml +++ b/infra/values/base/argocd-values.yaml @@ -6,10 +6,6 @@ configs: application.resourceTrackingMethod: annotation timeout.reconciliation: 60s admin.enabled: "true" - repositories: | - - type: git - url: https://github.com/snothub - name: github-repo params: "server.insecure": true server: @@ -32,7 +28,7 @@ notifications: method: POST body: | { - "payload": "🖥️ {{ .context.clusterName }}: 🔄 *{{ .app.metadata.name }}* is syncing...\n📦 Revision: {{ .app.status.sync.revision | substr 0 7 }}" + "payload": "🖥️ {{ .context.clusterName }}: 🔄 *{{ .app.metadata.name }}* is syncing...\n📦 Revision: {{ .app.status.sync.revision | default `n/a` | substr 0 7 }}" } template.app-sync-succeeded: | webhook: @@ -40,7 +36,7 @@ notifications: method: POST body: | { - "payload": "🖥️ {{ .context.clusterName }}: ✅ *{{ .app.metadata.name }}* sync succeeded\n📦 Revision: {{ .app.status.sync.revision | substr 0 7 }}{{ range .app.status.summary.images }}\n🏷️ Image: {{ . }}{{ end }}" + "payload": "🖥️ {{ .context.clusterName }}: ✅ *{{ .app.metadata.name }}* sync succeeded\n📦 Revision: {{ .app.status.sync.revision | default `n/a` | substr 0 7 }}{{ range .app.status.summary.images }}\n🏷️ Image: {{ . }}{{ end }}" } template.app-sync-failed: | webhook: @@ -48,7 +44,7 @@ notifications: method: POST body: | { - "payload": "🖥️ {{ .context.clusterName }}: ❌ *{{ .app.metadata.name }}* sync failed\n📦 Revision: {{ .app.status.sync.revision | substr 0 7 }}\n⚠️ Message: {{ .app.status.operationState.message }}" + "payload": "🖥️ {{ .context.clusterName }}: ❌ *{{ .app.metadata.name }}* sync failed\n📦 Revision: {{ .app.status.sync.revision | default `n/a` | substr 0 7 }}\n⚠️ Message: {{ .app.status.operationState.message }}" } template.app-degraded: | webhook: @@ -56,7 +52,7 @@ notifications: method: POST body: | { - "payload": "🖥️ {{ .context.clusterName }}: ⚠️ *{{ .app.metadata.name }}* is degraded\n🏥 Health: {{ .app.status.health.status }}\n💬 Message: {{ .app.status.health.message }}" + "payload": "🖥️ {{ .context.clusterName }}: ⚠️ *{{ .app.metadata.name }}* is degraded\n🏥 Health: {{ .app.status.health.status }}\n📦 Revision: {{ .app.status.sync.revision | default `n/a` | substr 0 7 }}{{ range .app.status.summary.images }}\n🏷️ Image: {{ . }}{{ end }}" } # Define notification triggers @@ -65,7 +61,7 @@ notifications: - when: app.status.operationState.phase in ['Running'] send: [app-syncing] trigger.on-sync-succeeded: | - - when: app.status.operationState.phase in ['Succeeded'] + - when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy' send: [app-sync-succeeded] trigger.on-sync-failed: | - when: app.status.operationState.phase in ['Failed'] diff --git a/infra/values/base/fluent-bit-values.yaml b/infra/values/base/fluent-bit-values.yaml index 0d00154..ee70788 100644 --- a/infra/values/base/fluent-bit-values.yaml +++ b/infra/values/base/fluent-bit-values.yaml @@ -48,7 +48,8 @@ config: Match kube.* Host loki-gateway.monitoring.svc.cluster.local Port 80 - Labels job=fluent-bit, namespace=$kubernetes['namespace_name'], pod=$kubernetes['pod_name'], container=$kubernetes['container_name'] + Labels job=fluent-bit, namespace=$kubernetes['namespace_name'], pod=$kubernetes['pod_name'], container=$kubernetes['container_name'], stream=$stream + Auto_Kubernetes_Labels Off Line_Format json [OUTPUT] diff --git a/infra/values/base/grafana-values.yaml b/infra/values/base/grafana-values.yaml index b2c8f48..9fdf61f 100644 --- a/infra/values/base/grafana-values.yaml +++ b/infra/values/base/grafana-values.yaml @@ -2,10 +2,10 @@ ingress: enabled: true resources: requests: - cpu: 100m + cpu: 50m memory: 128Mi limits: - cpu: 200m + cpu: 100m memory: 256Mi adminUser: admin @@ -25,6 +25,37 @@ datasources: uid: loki url: http://loki-gateway.monitoring.svc.cluster.local access: proxy + - name: Tempo + type: tempo + uid: tempo + url: http://tempo.monitoring.svc.cluster.local:3200 + access: proxy + jsonData: + tracesToLogsV2: + datasourceUid: loki + tags: + - key: namespace + - key: pod + - key: container + tracesToMetrics: + datasourceUid: Prometheus + tags: + - key: service.name + value: service + nodeGraph: + enabled: true + serviceMap: + datasourceUid: Prometheus + +sidecar: + dashboards: + enabled: true + label: grafana_dashboard + labelValue: "1" + folder: /tmp/dashboards + provider: + foldersFromFilesStructure: false + dashboardProviders: dashboardproviders.yaml: apiVersion: 1 @@ -43,1034 +74,3 @@ dashboards: gnetId: 15758 revision: 1 datasource: Prometheus - trivy: - json: | - { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - } - ] - }, - "description": "Monitore Trivy Image Vulnerabilities.\r\n\r\nBased on https://grafana.com/grafana/dashboards/17080-trivy-image-vulnerabilities/ and https://grafana.com/grafana/dashboards/16652-trivy-operator-reports/.", - "editable": true, - "fiscalYearStartMonth": 0, - "gnetId": 17214, - "graphTooltip": 1, - "id": 8, - "links": [], - "liveNow": false, - "panels": [ - { - "collapsed": false, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 43, - "panels": [], - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "A" - } - ], - "title": "Image Vulnerabilities", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "purple", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 0, - "y": 1 - }, - "id": 52, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "text": {}, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{severity=\"Unknown\", namespace=~\"$namespace\"})", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "UNKNOWN", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 4, - "y": 1 - }, - "id": 60, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{severity=\"Low\", namespace=~\"$namespace\"})", - "format": "time_series", - "hide": false, - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "LOW", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 8, - "y": 1 - }, - "id": 49, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{severity=\"Medium\", namespace=~\"$namespace\"})", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "MEDIUM", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "orange", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 12, - "y": 1 - }, - "id": 50, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{severity=\"High\", namespace=~\"$namespace\"})", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "HIGH", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 16, - "y": 1 - }, - "id": 51, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{severity=\"Critical\", namespace=~\"$namespace\"})", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "CRITICAL", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "text", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 20, - "y": 1 - }, - "id": 39, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"})", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "TOTAL", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 15, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "kube-system" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 4 - }, - "id": 58, - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "8.5.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"}) by (namespace)", - "instant": false, - "interval": "", - "legendFormat": "{{namespace}}", - "range": true, - "refId": "A" - } - ], - "title": "Image Vulnerabilities by namespace", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 15, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 1 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 4 - }, - "id": 61, - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "multi", - "sort": "desc" - } - }, - "pluginVersion": "8.5.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"}) by (severity)", - "instant": false, - "interval": "", - "legendFormat": "{{severity}}", - "range": true, - "refId": "A" - } - ], - "title": "Image Vulnerabilities by severity", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "transparent", - "mode": "fixed" - }, - "custom": { - "align": "left", - "cellOptions": { - "mode": "basic", - "type": "color-background" - }, - "filterable": true, - "inspect": false - }, - "links": [ - { - "targetBlank": true, - "title": "Go to CVE", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=${__data.fields.vuln_id}" - } - ], - "mappings": [ - { - "options": { - "CRITICAL": { - "color": "dark-red", - "index": 0 - }, - "HIGH": { - "color": "yellow", - "index": 1 - }, - "LOW": { - "color": "dark-blue", - "index": 3 - }, - "MEDIUM": { - "color": "dark-orange", - "index": 2 - }, - "UNKNOWN": { - "color": "super-light-blue", - "index": 4 - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "namespace" - }, - "properties": [ - { - "id": "custom.width" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "pod" - }, - "properties": [ - { - "id": "custom.width" - } - ] - } - ] - }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 12 - }, - "id": 67, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "frameIndex": 2, - "showHeader": true, - "sortBy": [ - { - "desc": false, - "displayName": "severity" - } - ] - }, - "pluginVersion": "11.0.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum(trivy_vulnerability_id{namespace=~\"$namespace\"}) without (namespace, last_modified_date, instance, job, endpoint, service, container, image_digest, resource, resource_name)", - "format": "table", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Image by CVE", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true, - "Value": true, - "instance": true, - "job": true - }, - "indexByName": { - "Time": 10, - "Value": 9, - "image_digest": 7, - "image_registry": 4, - "image_repository": 5, - "image_tag": 6, - "name": 8, - "namespace": 2, - "pod": 3, - "severity": 0, - "vuln_id": 1 - }, - "renameByName": { - "image_digest": "", - "name": "source workload" - } - } - } - ], - "type": "table" - } - ], - "refresh": "30s", - "schemaVersion": 39, - "tags": [ - "Prometheus", - "Addons", - "Trivy" - ], - "templating": { - "list": [ - { - "current": { - "selected": true, - "text": [ - "kube-system" - ], - "value": [ - "kube-system" - ] - }, - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "definition": "label_values(trivy_vulnerability_id{job=\"trivy-operator\"}, namespace)", - "hide": 0, - "includeAll": false, - "label": "namespace", - "multi": true, - "name": "namespace", - "options": [], - "query": { - "query": "label_values(trivy_vulnerability_id{job=\"trivy-operator\"}, namespace)", - "refId": "StandardVariableQuery" - }, - "refresh": 2, - "regex": "", - "skipUrlSync": false, - "sort": 1, - "type": "query" - } - ] - }, - "time": { - "from": "now-3h", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Trivy Operator / Image Vulnerability", - "uid": "trivy_starboard_operator", - "version": 7, - "weekStart": "" - } - dot-ai-logs: - json: | - { - "annotations": { "list": [] }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "title": "Log Volume", - "type": "timeseries", - "gridPos": { "h": 6, "w": 24, "x": 0, "y": 0 }, - "datasource": { "type": "loki", "uid": "loki" }, - "targets": [ - { - "expr": "sum(count_over_time({namespace=\"dot-ai\"} [1m])) by (pod)", - "legendFormat": "{{pod}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "bars", - "fillOpacity": 50, - "stacking": { "mode": "normal" } - } - }, - "overrides": [] - } - }, - { - "title": "Logs by Pod", - "type": "logs", - "gridPos": { "h": 16, "w": 24, "x": 0, "y": 6 }, - "datasource": { "type": "loki", "uid": "loki" }, - "targets": [ - { - "expr": "{namespace=\"dot-ai\", pod=~\"$pod\"} | json log=\"log\", message=\"message\", msg=\"msg\", level=\"level\", stream=\"stream\" | label_format level=`{{if .level}}{{.level}}{{else if eq .stream \"stderr\"}}error{{else}}info{{end}}` | line_format `{{.pod}} |{{or .message .msg .log}}`", - "refId": "A" - } - ], - "options": { - "showTime": true, - "showLabels": false, - "showCommonLabels": false, - "wrapLogMessage": true, - "prettifyLogMessage": false, - "enableLogDetails": true, - "sortOrder": "Descending", - "dedupStrategy": "none", - "displayedFields": ["pod", "level"] - } - }, - { - "title": "Errors & Warnings", - "type": "logs", - "gridPos": { "h": 10, "w": 24, "x": 0, "y": 22 }, - "datasource": { "type": "loki", "uid": "loki" }, - "targets": [ - { - "expr": "{namespace=\"dot-ai\", pod=~\"$pod\"} | json log=\"log\", message=\"message\", msg=\"msg\", level=\"level\", stream=\"stream\" | label_format level=`{{if .level}}{{.level}}{{else if eq .stream \"stderr\"}}error{{else}}info{{end}}` | level=~`error|warn|warning|fatal|panic` | line_format `{{.pod}} |{{or .message .msg .log}}`", - "refId": "A" - } - ], - "options": { - "showTime": true, - "showLabels": false, - "showCommonLabels": false, - "wrapLogMessage": true, - "prettifyLogMessage": false, - "enableLogDetails": true, - "sortOrder": "Descending", - "dedupStrategy": "none", - "displayedFields": ["pod", "level"] - } - } - ], - "schemaVersion": 39, - "tags": ["dot-ai", "logs", "loki"], - "templating": { - "list": [ - { - "name": "pod", - "type": "query", - "datasource": { "type": "loki", "uid": "loki" }, - "query": { "label": "pod", "stream": "{namespace=\"dot-ai\"}", "type": 1 }, - "includeAll": true, - "multi": true, - "current": { "selected": true, "text": "All", "value": "$__all" } - } - ] - }, - "time": { "from": "now-1h", "to": "now" }, - "title": "dot-ai Logs", - "uid": "dot-ai-logs" - } diff --git a/infra/values/base/keycloak-values.yaml b/infra/values/base/keycloak-values.yaml index 5047ee5..7599515 100644 --- a/infra/values/base/keycloak-values.yaml +++ b/infra/values/base/keycloak-values.yaml @@ -19,12 +19,18 @@ ingress: annotations: cert-manager.io/cluster-issuer: letsencrypt-prod +metrics: + enabled: true + prometheusRule: + namespace: monitoring + enabled: true + resources: requests: cpu: 250m memory: 512Mi limits: - cpu: "1" + cpu: 500m memory: 1Gi postgresql: @@ -56,5 +62,395 @@ keycloakConfigCli: "registrationAllowed": false, "loginWithEmailAllowed": true, "resetPasswordAllowed": true, - "rememberMe": true + "rememberMe": true, + "clients": [ + { + "clientId": "gitea", + "name": "Gitea", + "enabled": true, + "protocol": "openid-connect", + "clientAuthenticatorType": "client-secret", + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "publicClient": false, + "redirectUris": ["https://git.forteapps.net/*"], + "webOrigins": ["https://git.forteapps.net"], + "defaultClientScopes": ["openid", "email", "profile"], + "attributes": { + "k8s.secret.sync": "true", + "k8s.secret.namespace": "gitea", + "k8s.secret.name": "gitea-oidc-credentials", + "k8s.secret.client-id-key": "key", + "k8s.secret.client-secret-key": "secret" + }, + "protocolMappers": [ + { + "name": "email_verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.name": "email_verified", + "claim.value": "true", + "jsonType.label": "boolean", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + } + ] } + +extraDeploy: +# -- ServiceAccount for the client registrar CronJob +- apiVersion: v1 + kind: ServiceAccount + metadata: + name: keycloak-client-registrar + namespace: keycloak + +# -- ClusterRole granting access to secrets and namespaces +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: keycloak-client-registrar + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "create", "update", "patch"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] + +# -- ClusterRoleBinding for the registrar ServiceAccount +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: keycloak-client-registrar + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: keycloak-client-registrar + subjects: + - kind: ServiceAccount + name: keycloak-client-registrar + namespace: keycloak + +# -- CronJob: registers Keycloak clients and syncs secrets +- apiVersion: batch/v1 + kind: CronJob + metadata: + name: keycloak-client-registrar + namespace: keycloak + spec: + schedule: "*/2 * * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: keycloak-client-registrar + restartPolicy: Never + containers: + - name: registrar + image: alpine:3.20 + command: ["/bin/sh", "-c"] + args: + - | + set -e + apk add --no-cache curl jq > /dev/null 2>&1 + + KEYCLOAK_URL="http://keycloak:80" + REALM="forte" + K8S_API="https://kubernetes.default.svc" + SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + CA_CERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + CENTRAL_NS="secrets" + + # --- Authenticate to Keycloak Admin API --- + ADMIN_USER="admin" + ADMIN_PASS=$(cat /secrets/admin-password) + + echo "Authenticating to Keycloak..." + TOKEN=$(curl -sf -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ + -d "client_id=admin-cli" \ + -d "username=${ADMIN_USER}" \ + -d "password=${ADMIN_PASS}" \ + -d "grant_type=password" | jq -r '.access_token') + + if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + echo "ERROR: Failed to authenticate to Keycloak" + exit 1 + fi + + # --- Helper functions --- + + # Upsert a K8s Secret: try PUT (update), fall back to POST (create) + upsert_secret() { + local ns="$1" name="$2" manifest="$3" + local code + code=$(curl -sf -o /dev/null -w "%{http_code}" \ + --cacert "$CA_CERT" \ + -H "Authorization: Bearer ${SA_TOKEN}" \ + -H "Content-Type: application/json" \ + -X PUT -d "$manifest" \ + "${K8S_API}/api/v1/namespaces/${ns}/secrets/${name}") + if [ "$code" = "200" ]; then + echo " Updated secret '${ns}/${name}'" + elif [ "$code" = "404" ]; then + code=$(curl -sf -o /dev/null -w "%{http_code}" \ + --cacert "$CA_CERT" \ + -H "Authorization: Bearer ${SA_TOKEN}" \ + -H "Content-Type: application/json" \ + -X POST -d "$manifest" \ + "${K8S_API}/api/v1/namespaces/${ns}/secrets") + if [ "$code" = "201" ]; then + echo " Created secret '${ns}/${name}'" + else + echo " ERROR: Failed to create secret '${ns}/${name}' (HTTP ${code})" + return 1 + fi + else + echo " ERROR: Failed to update secret '${ns}/${name}' (HTTP ${code})" + return 1 + fi + } + + # Build a credential Secret JSON manifest + build_credential_secret() { + local ns="$1" name="$2" id_key="$3" secret_key="$4" b64_id="$5" b64_secret="$6" + cat < '${TARGET_NS}/${TARGET_NAME}' (keys: ${ID_KEY}, ${SECRET_KEY})" + sync_credentials "$CLIENT_ID" "$CLIENT_UUID" "$TARGET_NS" "$TARGET_NAME" "$ID_KEY" "$SECRET_KEY" + done + + # ============================================= + # NEW PATH — self-service config Secrets + # ============================================= + echo "" + echo "=== Self-service: config Secrets with label keycloak.forteapps.net/client-config=true ===" + + CONFIG_SECRETS=$(curl -sf \ + --cacert "$CA_CERT" \ + -H "Authorization: Bearer ${SA_TOKEN}" \ + "${K8S_API}/api/v1/namespaces/keycloak/secrets?labelSelector=keycloak.forteapps.net/client-config=true") + + CONFIG_COUNT=$(echo "$CONFIG_SECRETS" | jq '.items | length') + echo "Found ${CONFIG_COUNT} config Secret(s) to process" + + echo "$CONFIG_SECRETS" | jq -c '.items[]' | while read -r CONFIG_SECRET; do + CONFIG_NAME=$(echo "$CONFIG_SECRET" | jq -r '.metadata.name') + SOURCE_NS=$(echo "$CONFIG_SECRET" | jq -r '.metadata.annotations["keycloak.forteapps.net/source-namespace"] // .metadata.labels["keycloak.forteapps.net/source-namespace"] // "unknown"') + + # Decode client.json from the Secret data + CLIENT_JSON_B64=$(echo "$CONFIG_SECRET" | jq -r '.data["client.json"] // empty') + if [ -z "$CLIENT_JSON_B64" ]; then + echo "WARNING: Config Secret '${CONFIG_NAME}' missing client.json field, skipping" + continue + fi + CLIENT_JSON=$(printf '%s' "$CLIENT_JSON_B64" | base64 -d) + + CLIENT_ID=$(echo "$CLIENT_JSON" | jq -r '.clientId') + echo "Processing self-service client '${CLIENT_ID}' from config '${CONFIG_NAME}'" + + # Compute config hash for change detection + CONFIG_HASH=$(printf '%s' "$CLIENT_JSON" | sha256sum | cut -d' ' -f1) + EXISTING_HASH=$(echo "$CONFIG_SECRET" | jq -r '.metadata.annotations["keycloak.forteapps.net/config-hash"] // ""') + + # Extract secret delivery config from client.json + CRED_NS=$(echo "$CLIENT_JSON" | jq -r '.secret.namespace // "'"${SOURCE_NS}"'"') + CRED_NAME=$(echo "$CLIENT_JSON" | jq -r '.secret.name // "'"${CLIENT_ID}"'-oidc-credentials"') + CRED_ID_KEY=$(echo "$CLIENT_JSON" | jq -r '.secret.keys.clientId // "client-id"') + CRED_SECRET_KEY=$(echo "$CLIENT_JSON" | jq -r '.secret.keys.clientSecret // "client-secret"') + + # Check if credential Secret already exists in target namespace + CRED_EXISTS=$(curl -sf -o /dev/null -w "%{http_code}" \ + --cacert "$CA_CERT" \ + -H "Authorization: Bearer ${SA_TOKEN}" \ + "${K8S_API}/api/v1/namespaces/${CRED_NS}/secrets/${CRED_NAME}") + + # Skip if hash matches and credential Secret exists + if [ "$CONFIG_HASH" = "$EXISTING_HASH" ] && [ "$CRED_EXISTS" = "200" ]; then + echo " No changes detected, skipping" + continue + fi + + # Build Keycloak client representation (strip our secret delivery config) + KC_CLIENT=$(echo "$CLIENT_JSON" | jq '{ + clientId: .clientId, + name: .name, + enabled: true, + protocol: "openid-connect", + clientAuthenticatorType: "client-secret", + standardFlowEnabled: true, + directAccessGrantsEnabled: false, + publicClient: false, + redirectUris: .redirectUris, + webOrigins: .webOrigins, + defaultClientScopes: .defaultClientScopes, + protocolMappers: (.protocolMappers // []) + }') + + # Check if client already exists + EXISTING=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" \ + | jq -r '.[0].id // empty') + + if [ -n "$EXISTING" ]; then + echo " Updating existing Keycloak client (uuid: ${EXISTING})" + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -X PUT -d "$KC_CLIENT" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${EXISTING}") + if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "200" ]; then + echo " ERROR: Failed to update client '${CLIENT_ID}' (HTTP ${HTTP_CODE})" + annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/sync-status" "error" + continue + fi + CLIENT_UUID="$EXISTING" + else + echo " Creating new Keycloak client '${CLIENT_ID}'" + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -X POST -d "$KC_CLIENT" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients") + if [ "$HTTP_CODE" != "201" ]; then + echo " ERROR: Failed to create client '${CLIENT_ID}' (HTTP ${HTTP_CODE})" + annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/sync-status" "error" + continue + fi + # Fetch the newly created client's UUID + CLIENT_UUID=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" \ + | jq -r '.[0].id') + fi + + # Sync credentials to target namespace + sync_credentials "$CLIENT_ID" "$CLIENT_UUID" "$CRED_NS" "$CRED_NAME" "$CRED_ID_KEY" "$CRED_SECRET_KEY" + + # Annotate config Secret with hash and sync status + annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/config-hash" "$CONFIG_HASH" + annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/sync-status" "synced" + TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/last-sync" "$TIMESTAMP" + echo " Synced successfully" + done + + echo "" + echo "Client registrar run complete" + volumeMounts: + - name: keycloak-credentials + mountPath: /secrets + readOnly: true + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + volumes: + - name: keycloak-credentials + secret: + secretName: keycloak-credentials + items: + - key: admin-password + path: admin-password diff --git a/infra/values/base/loki-values.yaml b/infra/values/base/loki-values.yaml index 648d00c..3a5bd26 100644 --- a/infra/values/base/loki-values.yaml +++ b/infra/values/base/loki-values.yaml @@ -20,20 +20,22 @@ loki: limits_config: reject_old_samples: true reject_old_samples_max_age: 168h - ingestion_rate_mb: 10 - ingestion_burst_size_mb: 20 + ingestion_rate_mb: 15 + ingestion_burst_size_mb: 30 max_line_size: 512KB chunksCache: enabled: false +resultsCache: + enabled: false singleBinary: replicas: 1 resources: requests: - cpu: 100m - memory: 512Mi + cpu: 50m + memory: 256Mi limits: - cpu: 200m - memory: 2Gi + cpu: 100m + memory: 1Gi read: replicas: 0 backend: diff --git a/infra/values/base/prometheus-values.yaml b/infra/values/base/prometheus-values.yaml index 6b1307e..5fa1e1e 100644 --- a/infra/values/base/prometheus-values.yaml +++ b/infra/values/base/prometheus-values.yaml @@ -6,13 +6,15 @@ server: resources: requests: - cpu: 250m + cpu: 150m memory: 512Mi limits: - cpu: 500m + cpu: 300m memory: 1Gi enableLifecycle: true + extraFlags: + - web.enable-remote-write-receiver extraScrapeConfigs: | - job_name: kyverno @@ -56,5 +58,24 @@ extraScrapeConfigs: | - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_instance] target_label: instance + - job_name: traefik + scrape_interval: 15s + metrics_path: /metrics + kubernetes_sd_configs: + - role: endpoints + namespaces: + names: + - traefik-system + relabel_configs: + - source_labels: [__meta_kubernetes_endpoint_port_name] + regex: metrics + action: keep + - source_labels: [__meta_kubernetes_service_name] + target_label: service + - source_labels: [__meta_kubernetes_pod_name] + target_label: pod + - source_labels: [__meta_kubernetes_namespace] + target_label: namespace + alertmanager: enabled: false diff --git a/infra/values/gitea-actions-values.yaml b/infra/values/gitea-actions-values.yaml new file mode 100644 index 0000000..c659ec1 --- /dev/null +++ b/infra/values/gitea-actions-values.yaml @@ -0,0 +1,36 @@ +## Gitea Act Runner - Helm values +## Chart: actions v0.0.5 (https://dl.gitea.com/charts) + +enabled: true + +giteaRootURL: https://git.forteapps.net + +existingSecret: gitea-runner-token +existingSecretKey: token + +statefulset: + replicas: 3 + + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + + actRunner: + config: | + log: + level: info + cache: + enabled: false + container: + require_docker: true + docker_timeout: 300s + runner: + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" + - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" + dind: + rootless: false diff --git a/infra/values/gitea-values.yaml b/infra/values/gitea-values.yaml new file mode 100644 index 0000000..e34f256 --- /dev/null +++ b/infra/values/gitea-values.yaml @@ -0,0 +1,181 @@ +# Gitea Helm Chart Values +# Host: git.forteapps.net +# Chart: gitea v12.5.0 (app v1.25.4) +# Repo: https://dl.gitea.com/charts + +# -- Admin account (password from sealed secret) +gitea: + admin: + existingSecret: gitea-credentials + email: admin@forteapps.net + + # -- Gitea app.ini configuration + config: + APP_NAME: "Forte Git" + + server: + DOMAIN: git.forteapps.net + ROOT_URL: https://git.forteapps.net + SSH_DOMAIN: git.forteapps.net + SSH_PORT: 2222 + LFS_START_SERVER: true + ENABLE_GITEA_PAGES: true + ENABLE_BASIC_AUTH_CHALLENGE: true + + service: + DISABLE_REGISTRATION: false + DEFAULT_ALLOW_CREATE_ORGANIZATION: false + REQUIRE_SIGNIN_VIEW: false + ALLOW_ONLY_EXTERNAL_REGISTRATION: true + ENABLE_BASIC_AUTHENTICATION: true + ENABLE_PASSWORD_SIGNIN_FORM: false + ENABLE_NOTIFY_MAIL: true + + openid: + ENABLE_OPENID_SIGNIN: false + ENABLE_OPENID_SIGNUP: false + + oauth2: + ENABLED: true + ENABLE_AUTO_REGISTRATION: true + USERNAME: email + + session: + PROVIDER: db + + cache: + ADAPTER: memory + + database: + DB_TYPE: postgres + + metrics: + ENABLED: true + + repository: + DEFAULT_BRANCH: main + DEFAULT_PRIVATE: last + + actions: + ENABLED: true + + packages: + ENABLED: true + + indexer: + ISSUE_INDEXER_TYPE: bleve + REPO_INDEXER_ENABLED: true + + mailer: + ENABLED: true + PROTOCOL: smtp+starttls + SMTP_ADDR: smtp.office365.com + SMTP_PORT: 587 + FROM: "noreply@fortedigital.com" + + admin: + DEFAULT_EMAIL_NOTIFICATIONS: enabled + + # -- SMTP credentials injected from secret (USER and PASSWD) + additionalConfigFromEnvs: + - name: GITEA__mailer__USER + valueFrom: + secretKeyRef: + name: gitea-smtp-secret + key: username + - name: GITEA__mailer__PASSWD + valueFrom: + secretKeyRef: + name: gitea-smtp-secret + key: password + # -- OIDC authentication via Forte + oauth: + - name: "Forte" + provider: "openidConnect" + existingSecret: gitea-oidc-credentials + key: gitea + autoDiscoverUrl: "https://id.forteapps.net/realms/forte/.well-known/openid-configuration" + scopes: "openid email profile organization" + groupClaimName: "groups" + adminGroup: "" + restrictedGroup: "" + # -- Prometheus metrics (scraped via annotations, no ServiceMonitor CRD needed) + metrics: + enabled: true + serviceMonitor: + enabled: false + +# -- Ingress via Traefik with Let's Encrypt TLS +ingress: + enabled: true + className: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: git.forteapps.net + paths: + - path: / + pathType: Prefix + tls: + - secretName: gitea-tls + hosts: + - git.forteapps.net + +# -- Git repository storage +persistence: + enabled: true + size: 10Gi + accessModes: + - ReadWriteOnce + storageClass: upcloud-block-storage-maxiops + +# -- Recreate strategy to avoid Multi-Attach errors with RWO volumes +strategy: + type: Recreate + +# -- Pod resources +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +# -- Embedded PostgreSQL (Bitnami subchart) +# Password auto-generated by the subchart; Gitea chart auto-wires the connection. +postgresql: + enabled: true + auth: + username: gitea + database: gitea + primary: + persistence: + enabled: true + size: 8Gi + storageClass: upcloud-block-storage-maxiops + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +# -- Disable PostgreSQL HA (using single-instance postgresql above) +postgresql-ha: + enabled: false + +# -- Disable Redis cluster (use in-memory cache instead) +redis-cluster: + enabled: false + +# -- Disable test pod +test: + enabled: false + +# -- SSH service (ClusterIP, exposed externally via Traefik TCP IngressRoute on port 2222) +service: + ssh: + type: ClusterIP + port: 22 diff --git a/infra/values/opencost-values.yaml b/infra/values/opencost-values.yaml new file mode 100644 index 0000000..39d73cc --- /dev/null +++ b/infra/values/opencost-values.yaml @@ -0,0 +1,30 @@ +opencost: + exporter: + defaultClusterId: launchpad + extraEnv: + EMIT_KSM_V1_METRICS: "false" + EMIT_KSM_V1_METRICS_ONLY: "true" + prometheus: + internal: + enabled: true + serviceName: prometheus-server + namespaceName: monitoring + port: 80 + customPricing: + enabled: true + provider: custom + costModel: + description: "UpCloud 4-node cluster pricing" + CPU: "5.86" + RAM: "1.46" + GPU: "0" + storage: "0.34" + zoneNetworkEgress: "0" + regionNetworkEgress: "0" + internetNetworkEgress: "0" + ui: + enabled: false +service: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9003" diff --git a/infra/values/renovate-values.yaml b/infra/values/renovate-values.yaml new file mode 100644 index 0000000..1ec12eb --- /dev/null +++ b/infra/values/renovate-values.yaml @@ -0,0 +1,45 @@ +cronjob: + schedule: "@daily" + concurrencyPolicy: Forbid + +renovate: + config: | + { + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "platform": "gitea", + "endpoint": "https://git.forteapps.net", + "autodiscover": true, + "gitAuthor": "Renovate Bot ", + "packageRules": [ + { + "matchRepositories": ["**/10x"], + "assignees": ["edvard.unsvag"], + "reviewers": ["edvard.unsvag"] + }, + { + "matchRepositories": ["**/auth-sidecar"], + "assignees": ["danijel.simeunovic"], + "reviewers": ["danijel.simeunovic"] + }, + { + "matchRepositories": ["**/forte-helm"], + "assignees": ["danijel.simeunovic"], + "reviewers": ["danijel.simeunovic"] + } + ] + } + +envFrom: +- secretRef: + name: renovate-env + +env: + LOG_LEVEL: info + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "2" + memory: 4Gi diff --git a/infra/values/tempo-values.yaml b/infra/values/tempo-values.yaml new file mode 100644 index 0000000..0ba785d --- /dev/null +++ b/infra/values/tempo-values.yaml @@ -0,0 +1,34 @@ +tempo: + metricsGenerator: + enabled: true + remoteWriteUrl: "http://prometheus-server.monitoring.svc.cluster.local/api/v1/write" + overrides: + defaults: + metrics_generator: + processors: + - service-graphs + - span-metrics + storage: + trace: + backend: local + local: + path: /var/tempo/traces + receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +persistence: + enabled: true + size: 10Gi + +resources: + requests: + cpu: 50m + memory: 256Mi + limits: + cpu: 100m + memory: 512Mi diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..57a01e4 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,43 @@ +site_name: K8s Launchpad +site_description: Documentation for the GitOps-managed Kubernetes cluster +repo_url: https://git.forteapps.net/Forte/launchpad +repo_name: Forte/launchpad + +theme: + name: material + palette: + - scheme: default + primary: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.instant + - navigation.sections + - navigation.top + - search.highlight + - content.code.copy + +nav: +- Home: README.md +- GitOps Architecture: GITOPS-ARCHITECTURE.md +- Developer Guide: DEVELOPER-GUIDE.md +- Operations Runbook: OPERATIONS-RUNBOOK.md +- Technical Reference: REFERENCE.md + +markdown_extensions: +- tables +- toc: + permalink: true +- pymdownx.highlight: + anchor_linenums: true +- pymdownx.superfences +- pymdownx.tabbed: + alternate_style: true +- admonition +- pymdownx.details diff --git a/scripts/gitea-backup.sh b/scripts/gitea-backup.sh new file mode 100644 index 0000000..397a6fa --- /dev/null +++ b/scripts/gitea-backup.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Gitea backup helper — interacts with the S3 bucket via a temporary pod +# Uses the gitea-backup-s3 secret in the gitea namespace +# +# Usage: +# ./scripts/gitea-backup.sh list # list all backups +# ./scripts/gitea-backup.sh download # download a backup to current dir +# ./scripts/gitea-backup.sh download latest # download the most recent backup + +NAMESPACE="gitea" +SECRET="gitea-backup-s3" +IMAGE="minio/mc:latest" +POD_NAME="gitea-backup-helper" +ALIAS_CMD='mc alias set upcloud ${S3_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} > /dev/null' + +cleanup() { + kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true +} + +mc_run() { + cleanup + kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \ + --image="$IMAGE" \ + --overrides="{ + \"spec\":{\"containers\":[{ + \"name\":\"$POD_NAME\", + \"image\":\"$IMAGE\", + \"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}], + \"command\":[\"sh\",\"-c\",\"${ALIAS_CMD}; $1\"], + \"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}] + }]} + }" > /dev/null 2>&1 + + kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1 + kubectl -n "$NAMESPACE" logs "$POD_NAME" + cleanup +} + +case "${1:-help}" in + list) + echo "Listing backups..." + mc_run 'mc ls upcloud/${S3_BUCKET}/' + ;; + + download) + FILE="${2:?Usage: $0 download }" + + if [ "$FILE" = "latest" ]; then + echo "Finding latest backup..." + FILE=$(mc_run 'mc ls upcloud/${S3_BUCKET}/' | sort | tail -1 | awk '{print $NF}' | tr -d '[:space:]') + if [ -z "$FILE" ]; then + echo "No backups found." + exit 1 + fi + echo "Latest: $FILE" + fi + + echo "Downloading $FILE..." + cleanup + kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \ + --image="$IMAGE" \ + --overrides="{ + \"spec\":{\"containers\":[{ + \"name\":\"$POD_NAME\", + \"image\":\"$IMAGE\", + \"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}], + \"command\":[\"sh\",\"-c\",\"sleep 300\"], + \"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}] + }]} + }" > /dev/null 2>&1 + + kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1 + + echo "Saving to ./$FILE ..." + kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${ALIAS_CMD} && mc cat upcloud/\${S3_BUCKET}/$FILE" > "./$FILE" + cleanup + + echo "Downloaded: ./$FILE" + ;; + + *) + echo "Gitea backup helper" + echo "" + echo "Usage:" + echo " $0 list List all backups in S3" + echo " $0 download Download a specific backup" + echo " $0 download latest Download the most recent backup" + ;; +esac diff --git a/scripts/gitea-restore.sh b/scripts/gitea-restore.sh new file mode 100644 index 0000000..34c9ac7 --- /dev/null +++ b/scripts/gitea-restore.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Gitea restore helper — restores a gitea-dump zip into a running Gitea deployment +# +# Prerequisites: +# - Gitea deployed on the target cluster (Helm chart via ArgoCD) +# - kubectl context pointing to the target cluster +# - A gitea-dump zip file (from gitea-backup.sh download or CronJob) +# +# Usage: +# ./scripts/gitea-restore.sh +# +# What it does: +# 1. Scales Gitea down to 0 +# 2. Restores the PostgreSQL database from gitea-db.sql +# 3. Restores Git repositories to the data PVC +# 4. Scales Gitea back up + +NAMESPACE="gitea" +DEPLOYMENT="gitea" +PG_STATEFULSET="gitea-postgresql" +PVC="gitea-shared-storage" +HELPER_POD="gitea-restore-helper" +HELPER_IMAGE="alpine:3.20" +PG_USER="gitea" +PG_DB="gitea" + +DUMP_FILE="${1:?Usage: $0 }" + +if [ ! -f "$DUMP_FILE" ]; then + echo "Error: file not found: $DUMP_FILE" + exit 1 +fi + +echo "=== Gitea Restore ===" +echo "Dump file: $DUMP_FILE" +echo "" + +# --- Safety prompt --- +read -r -p "This will OVERWRITE the Gitea database and repositories on the current cluster. Continue? [y/N] " confirm +if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 +fi + +cleanup() { + kubectl -n "$NAMESPACE" delete pod "$HELPER_POD" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true +} +trap cleanup EXIT + +# --- Step 1: Extract dump locally --- +RESTORE_DIR=$(mktemp -d) +echo "" +echo "[1/5] Extracting dump..." +unzip -q "$DUMP_FILE" -d "$RESTORE_DIR" + +# Detect SQL dump file (gitea-db.sql or gitea-dump-*.sql) +SQL_FILE=$(find "$RESTORE_DIR" -maxdepth 1 -name '*.sql' | head -1) +if [ -z "$SQL_FILE" ]; then + echo "Error: no .sql file found in dump." + rm -rf "$RESTORE_DIR" + exit 1 +fi + +# Detect repos — either a "repos" folder or gitea-repo.zip +if [ -d "$RESTORE_DIR/repos" ]; then + REPOS_SOURCE="dir" + REPOS_PATH="$RESTORE_DIR/repos" +elif [ -f "$RESTORE_DIR/gitea-repo.zip" ]; then + REPOS_SOURCE="zip" + REPOS_PATH="$RESTORE_DIR/gitea-repo.zip" +else + echo "Error: no repos/ directory or gitea-repo.zip found in dump." + rm -rf "$RESTORE_DIR" + exit 1 +fi + +echo " Found: $(basename "$SQL_FILE") ($(du -h "$SQL_FILE" | cut -f1))" +echo " Found: repos ($REPOS_SOURCE)" +[ -d "$RESTORE_DIR/data" ] && echo " Found: data/" +[ -f "$RESTORE_DIR/app.ini" ] && echo " Found: app.ini (managed by Helm, skipping)" + +# --- Step 2: Scale down Gitea --- +echo "" +echo "[2/5] Scaling down Gitea..." +kubectl -n "$NAMESPACE" scale deployment "$DEPLOYMENT" --replicas=0 +kubectl -n "$NAMESPACE" rollout status deployment "$DEPLOYMENT" --timeout=60s 2>/dev/null || true +echo " Waiting for pods to terminate..." +kubectl -n "$NAMESPACE" wait --for=delete pod -l app.kubernetes.io/name=gitea --timeout=120s 2>/dev/null || true +echo " Gitea is down." + +# --- Step 3: Restore database --- +echo "" +echo "[3/5] Restoring database..." +# Drop and recreate to ensure clean state +kubectl -n "$NAMESPACE" exec "${PG_STATEFULSET}-0" -- \ + psql -U "$PG_USER" -d postgres -c "DROP DATABASE IF EXISTS ${PG_DB};" 2>/dev/null +kubectl -n "$NAMESPACE" exec "${PG_STATEFULSET}-0" -- \ + psql -U "$PG_USER" -d postgres -c "CREATE DATABASE ${PG_DB} OWNER ${PG_USER};" 2>/dev/null +kubectl -n "$NAMESPACE" exec -i "${PG_STATEFULSET}-0" -- \ + psql -U "$PG_USER" -d "$PG_DB" < "$SQL_FILE" > /dev/null +echo " Database restored." + +# --- Step 4: Restore repositories --- +echo "" +echo "[4/5] Restoring repositories..." + +# Need a helper pod on the same node as the PVC (RWO) +cleanup +kubectl -n "$NAMESPACE" run "$HELPER_POD" --restart=Never \ + --image="$HELPER_IMAGE" \ + --overrides="{ + \"spec\":{ + \"containers\":[{ + \"name\":\"$HELPER_POD\", + \"image\":\"$HELPER_IMAGE\", + \"command\":[\"sleep\",\"3600\"], + \"volumeMounts\":[{\"name\":\"data\",\"mountPath\":\"/data\"}] + }], + \"volumes\":[{ + \"name\":\"data\", + \"persistentVolumeClaim\":{\"claimName\":\"$PVC\"} + }] + } + }" > /dev/null 2>&1 + +kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$HELPER_POD" --timeout=120s > /dev/null 2>&1 + +# Clear existing repos +kubectl -n "$NAMESPACE" exec "$HELPER_POD" -- sh -c "rm -rf /data/gitea/repositories/*" 2>/dev/null || true + +# Upload repos — tar via stdin since kubectl cp needs tar in the container +echo " Uploading repositories..." +if [ "$REPOS_SOURCE" = "dir" ]; then + # repos/ directory — tar and stream into the PVC + tar -cf - -C "$REPOS_PATH" . | kubectl -n "$NAMESPACE" exec -i "$HELPER_POD" -- sh -c "tar -xf - -C /data/gitea/repositories/" +else + # gitea-repo.zip — stream and extract + cat "$REPOS_PATH" | kubectl -n "$NAMESPACE" exec -i "$HELPER_POD" -- sh -c "cat > /tmp/gitea-repo.zip && unzip -q -o /tmp/gitea-repo.zip -d /data/gitea/repositories/ && rm /tmp/gitea-repo.zip" +fi + +# Restore data/ directory (avatars, attachments, LFS, etc.) if present +if [ -d "$RESTORE_DIR/data" ]; then + echo " Uploading data (avatars, attachments, LFS)..." + tar -cf - -C "$RESTORE_DIR/data" . | kubectl -n "$NAMESPACE" exec -i "$HELPER_POD" -- sh -c "tar -xf - -C /data/gitea/" +fi + +# Fix ownership +kubectl -n "$NAMESPACE" exec "$HELPER_POD" -- chown -R 1000:1000 /data/gitea/ +echo " Repositories restored." + +# --- Step 5: Scale back up --- +echo "" +echo "[5/5] Scaling Gitea back up..." +kubectl -n "$NAMESPACE" scale deployment "$DEPLOYMENT" --replicas=1 +kubectl -n "$NAMESPACE" rollout status deployment "$DEPLOYMENT" --timeout=120s + +# Cleanup temp dir +rm -rf "$RESTORE_DIR" + +echo "" +echo "=== Restore complete ===" +echo "Gitea should be back online with restored data." +echo "Verify at your Gitea URL that repos and users are present." diff --git a/secrets/argocd-forte-helm-secret-sealed.yaml b/secrets/argocd-forte-helm-secret-sealed.yaml new file mode 100644 index 0000000..9d17e84 --- /dev/null +++ b/secrets/argocd-forte-helm-secret-sealed.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: forte-helm-repo + namespace: argocd +spec: + encryptedData: + project: AgBBeKBtro27DMJ+9YFjSlvv+XWyotBojRbaDVPDfwH0MoXST9zUKZSXDgbuJ5sy8dQYhfCZgtfjcNpWbCmLoURZMm1aPa0U05bG7Hz2EnO+39qcgpH/jzEzp0AsZ9YgMafgI0EyL63RnVmvvcFT81fNe4P6vlKjz+dyhIuXbELhDjabNE+QwsLmQkty7R6isJSx42+aEGliD69FcFjlvwz75L7y2Rck1eodUVbaTX3qdG/bSFCTWawCqnj27oEn+qYwcvrS/B8MsiG8XkcmAxy3QVHAW4waBQ/6IALqBaviXunye7TOujgfn996sQWEZz9e6tVsbh1I4i/1T44rtqiot3EuFMMuYN1C6JMb53lUI9KM/TMWM3U/B/MgFR7DbXRF8/VRkBqVJFv/PZWKxTNbnYEhdPBPZojZNhHntdFZNw7l9Pr251mPI9UCU+Mc+RKtluKb+c40C9Ty1TGaV0DQ3UtA+1zbotDVGx21P7tAwplz22czu0GSUZu3nDDEPoyQoI1YG4TBvqKwuFbxJZASUglDmwyzFG15QIn03zE661ETR1ETXMbqxJR8yWmY7G5R1aNMCX9oc5l+xS6lUuUsI/fmN58VA3i82j4jEB/+oPyQ6bxsiteGXkvIz4ONxh2Ve+FmgxaDNLJNH0RHnJ7J/mJARfn43o264ONq7eTt2HuOgoP9UIsOPXuDmlBo1oZ0zxWXUEST + sshPrivateKey: AgBNgdWFu4x9CEgKftriZCnjSPNgV+RpAxi8/N0XawXATbQEtsMHfctTsRtwsOvaEvCehlLq7dJWRmPiuTGZ7fA2vaetoj7ppa59kHJtEO4teVcU5n64glLV5EqZbwm/nel0b5ri+otrY2zjo/tomt/7e0K30OZbe3Wghr6pDH8MrScAxO+VqgJHqqXeFayY2cC4e/mZXGu7J+FKBl3si/i6nKtKFhaqCKF+Wn4tou7wZDIdx4A+jWUxTnQquQU8IdaZof0hUNKrmmD1ayK5mJxfOBLqt9TXoTKGwkZJkdr0Pvo8tx1DqTCfpyL9Ur7B7H4G5x9n+AaD6+Iwn0eohty+NPLoy3v6JYYayZb9Foi41EzRHm+W9Cc0TcDBNTN8kR9u713n+YNuFHCaALx4iR28svfKHHjsOEzZ5688WXuph2+eLm+DW03obRDGJYi25j9URYX31LJSTMEs0rhhsUn3eeipUjQ2Fdo3xXFnALLOci0XjRd6YvuR5DN1pkj4KFcMzHvTkyM8jG9eTHGloW55zFTNtb9QkNz+x75RRpCmNlT0bKqAjxfov70K8gbSIzZ7d77iai68JQ/FnTjuL8Gx2Jyr/ltlVnql4fpv1u691SCCTvoz6aAVknn63SXdrnJ9hv+/Nb01MZhYAciYgvAAoxAOT3UOpIBnJ5h/VUacJ+Op26jtSsWrMjAtVSN2RjqzXAbVe/HONpE76vEfDHKizHEUriT7y1WoOUxcbwGIFAzSDM2OidUW/SstCNCmGQ9Whdq427X+eQIh2h+EpCPYXy3YTuWfSISgkA74bSVeX+2mHpwgo+5GPfiAQmHajdZLYTo+GAaty7VdNWTL1AEdgFkYR4TwaeR8DcLe03efg5Q1SLd1I0+kjtkVQcWsnTnchnMcXK1tvOAcNc9MY9pKNh/gW/nAnsAyfEA9opl2LFyywpeDizDdowO99meiJB7+ISS1lOI5f7xvcM6ovEcTugj4Dwv05546Zxb1ziIGbhq5kiD5FIMCPzBo0vX4XXeUa0GRIVCHRxgygi17bTMwKGKRKGusduBVbc/+OKaX5ciAtxPSq85Z6XCXSEXzajsIFkasQtm7tt44U80fAlwmgAfGdRQMF5CcEW7x5WyXVb6mZUt3GgQmPdfrP6kTgaxXGTkGsbb5bcsbf3DeoAdiJ1rJPBVX4z10cJsdRCKI+d0VwVLu59eheOajdvPxdhR+tNhJfavaUKtj7THrMJUq7WPZq4GlPQpO7S3+Q6V1e6eaSw== + type: AgBf+Wb64fdsnUEwpxRcWg8fRtMi8MisZOA9gm/uwA2u+OZUNh/vHO5hzjxG3NVeT+6sOM3iJJhnLHTv7nxykP//3qG/PqkVPPYPvGm5v8sd698Ga146FsOKQzga7mn8vGZ2FYyZmfq2l3ZBS2j+12M6TtXw5a4dqCtrVq5FbvBr9MBKTxpHdKnpgddxyWYxE9uSgFm+2IrT9hH7OAhke0NrQQwbDFpOI/y2xhgaIGp/E78G+8Ijwc0ofrd8XIjKWcesXD4uQqTBHZfcoanMWlSqrfXDxSRiBNVkHot1AnE8DiukL3+Dntxr6bh3bIZWvsHuNa9fQnjH6BNskYQSYopreHdujsvYDm19vqHNpkW1mTmjdimWgxX5Ot2kzcnuguA4Qn2JjpLicT2GbNIEcI6rjghTM6CATOFWmiF933uIXhDN9RlfMR/+hTLisFvnZmdsH/6byvoe39UxavtVEKsSPCa3h2FN5O2HKrccWrAZ9fQdDtIzwVMXi48rsi5sB05By2WetsUMu3JdRlM8noQ3qtrKzdtjyQIUFJcIxRkL8mKz5lkXPKjn7qlItUY5VG6dnhHQRTxYdAxn3jKMpYh0jEtloI3m3SEXhfE9JPbA4RIgNi8evnRBMKwBwEr8mU52DbB1J3gU7Gm9IOYBsOgq0U1BkzqCqIaThoUWXCNGkAG3rc7uLuyOaHHVEBLsmu+m1cE= + url: AgB3C9i/Ue7oHmeoLQCr8BVYIqYIT8EmrhkFjj+vjBSTynYwoFonaiPMqDJ2XewJV9WpO6wL0IYH/cK0BkhbPhjN6oQ88Qn1MkuVCjtGht1cUeOzRtSE8hdqoOKaagFb4pcgwwm8ldU7bry3KioXnz36YaU7wcddUkHVQYQOp6DvcxtjonsLUFHecDcW+qL5aVzwI3CBkn0xMrc2kRiO2q2ZjR7BNNddNUw5f1v3fYNE0PARd+bVyegSEMuBRoh1amautWEfuYoCENPqhfd9Umbs9eml/j86pj9mvkVZ2CzRt78V+C3ciMZM6QlLV+BdiPm99/igyvZScDEB/tUrMaj/K6qhMYG7r8jNBQrsg3luvTVFcC75e8aSgfRJ0QDiYmtZ5fP+8AS+l2pyRFMkbOMAk62LCqAiXuXk/vC412Lrk4NkEIxqCDnf771PTOkbXYXNlHzC2w1AZSkvPqKjIvJbLCkCxGFVBeUmlZzcCIYDeXTpIRwZsVIo+sGpjbR8soeTb8suw6GqYjgXPhXm/S1kpL6yAXfCFCXmyxntMZCuWuFPPWfwzn5GDfSJS7Hz43cuh7vwrQ78xC1wQRs7EObWNUbLIfvWEypvncYmixKY8+02SrDwzcwADimJKMxs7Bp10rkMO4HkpcFDisVyHCgBbfG3BYFo0sT5+FN6b5Bhuo6hUaCzEe9HAeSIFNuKaX5wYbY1MzsRoWOvydStoQa9KwVeZwBPzZV0VW2c3wMRT1pNN4s2THGlphw= + template: + metadata: + creationTimestamp: null + labels: + argocd.argoproj.io/secret-type: repository + name: forte-helm-repo + namespace: argocd diff --git a/secrets/dot-ai-secrets-sealed.yaml b/secrets/dot-ai-secrets-sealed.yaml new file mode 100644 index 0000000..5dd9590 --- /dev/null +++ b/secrets/dot-ai-secrets-sealed.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: dot-ai-secrets + namespace: dot-ai +spec: + encryptedData: + anthropic-api-key: AgAdnPQzl2SNSqyAZkoBdEEGEjXfpMJUNMg4e040vxri4jbjoTnmwQ/DjmmIGtoInjhQA3NZRUKcIuDrO7nGMXFOPY9tSfhpdaAMWTji1CVZvmdJnrQwZY1LFBwgbozq3RSZZQQcZd2EPrhhsU5UlmacKk2WpuQdq76twfdU5WVD8xalxbt0H5UDvihuQa3sYX/rUIDZK2Th5IfaFqYZk5JvaxR8PFwBh9IOooGewWXWrsxVT510lxHRotSDIgKt/jl8ymsVA7IpddnbQHr6K77it7BxR6fLvDGaTQeQW0RtPZwsLvB268OTwp338RQ2sjmVuUDErWViLuk1V6UFmj5vhRR0X2dy/EipkWEJwR0Zf8JCW+vAXJ4NFiVFjmOsiTXoLHCYp0k01EwFcPQpnswKSdaQ3tuCEa4Q+rHu2DiXyfSuZ1MnWnTyLSx9H51VYUgIPwQa+BUVoVnAswypw4yLQHrfLu9TSvsNl/OonP0dSmgSGaByTc1RzwpvHBUAaOYII2LuuvkF90oA4cChGkGe1KuYKHFqiBdaT9jrsfeBUpgqFAH0uH9DNjBjU5EjlUtV3bdO7wXDHct9EOBZniKMH7cIbXfT2TBaormeHNqFtXxSRXy1szDpXiDD1QDoAJx0ylrwwqix7XPBlqSt91OZ8N1VPSUPvfnTQf7zirVWJNckU9fW6tsnQ+GDOOEwERPEAhqxWuw6V0HMab4VzpbGgWuvtQY5CsI2bFUYWSZAIJ6H8E5ip3lb8ilWtYadhx8QZD3aKi7k5h4bFWd6M0rg0uj7bfbW8YXauTS6Uz4b9FlvgrAPkmcWG84um/jsmP6a0YhvJDGKHMHoUmY= + auth-token: AgBq/MM/+fqi1H6L5o/xFeKYMfcn6+bmbkepbq97O0Ob+dFKWuyfXmzqM2Dha+NKpXcu47qSBbLpaOdLS5ONq7JDIcZ2byW0ae9khBuL7k2mCa5ZPh96LS46c+v1qNJb6F4NQe+jWSx2H9LIShoihom+lRueBT1/uthW3hnUUUgQMgXU3NYimDNAg6JK6VgX3yKkI7ePLPAet7+ykaL1aPXwcfCAldcobPmls0vxMQDtgd+o4nLqx2sKArFplnwZ9G1SEz2dRXNFm8HI4HhgtOBdv1ISjXO8XGRZWnFqgX3s3BqcwFcPqVuFGHo+2ZvAUW+mdkuAEJOloJEoZXJkFCP+l7/yMV7/FmFAhntqfdTwijEJ+rP/sUbUNSzn6BKM2ITRGXEJkOeNq4xfpgS16AOs+O0DWkIAr0kWCJG+mkn94U7ALCGYQXtKLy0g/87MJ3IwaUU7gowVawHoDmpY0QoAYDJwpbIa9yMnlPOzJANkc99dl85sbLeQMvkkb4RqQlbMc80gkFFhbU5/0kKX483ylqhRi0b4ZjGHgl72s/8+FhxTDkSXJQ2HbrNPGlna+B7TAgxYZ9IS1cpw4wSNzSCk23Rt3STHnXuu/QXRLeMijXGBDFNJgU0JNKLp63JC1gftHcNhqRL6lxK1tLyo5+0Cnh/KJN64b1DpPXZnzdaeR25GQFSDxsymjLm2BFATtufRPJqeIT7tn5TRGTr97fH6LQv/LuJ4VnjS6WLsQoFdUhDuNug2LDuyLHjpKg== + openai-api-key: AgCQR+HezEHwiwFD1pIkla1Byzz91SBoUyaWVTTuBrBDq201KvlLmqahjQwpKWs4YvqhmKkbF/mXKuMSyFut72IwH025tfmzzmTryOD6bOQDxI0Ws5NndnztzKv2qw2Js0c+6lAOJR+lEuo1ynGlG3hfS+bGShShvLZIDcD/hDa8IfhQBh7fKyFslJ52KxKK+zeEFDVQcx6Lrq/zP9IPH2QXz0bVQYlRyrQ4vqONO9h2pjxNAhDxuJ5FPk+Hb4ClGjyRPHFT34ZYsYdvHUgkjL3nOeaOUrc0GGJOKYZChfsK/JrqX8iOqgbE+0vQvMX+q8Tr6Ynr0KRKMcGPr6ak3eCNknJaZ1EABtkyUS2u5TzKqMzYq5DF4kpvPApIvaQW+VS05zD8piTQSZEFNRwwXDpyru/R4MmPvwt7OdSM0SJpQ8kR7tsuRgBPFY2Eduk6fhrDmMVgHze3AoRaqyh87CeqR3Z7/XofbdgZBqEp2vwiY425ArB6yMLpVvO6eB+yoTgCJ/UpfOxBDHE1M6U3goJhk/YeOy9UpTfRD1VyA2OuzJliDNtC784wxnGAsOk3qKk1bjjBqUzwITK41mOWqNLKCn4Ol9lXZtbbqLIiU4lXv4Pl93mVSuPLOIXINLGMRFl0rlvDICEjrrfE+JmwKx6i45sqsk3/hz1Fb+/bKFo7pl9JnAXvr5WfDW/rflwBrokfJVeYsSm6R2yxGWlosrKZIwIRctOut1EbbylurI5pIPp6vHguyIvZDDxwj3Qk3ewU/WVAcI9989aLR++UrZ5FGXt3apMk8BY9bq7DIQOUtgfPO98+cVPdoZSIh5bhQpCHAVPMGcGTdIPkuQbxSai0FsP8ipdHx0fluI6p/4v1Rv/BqZH0X8dnwPQoqUhSD+C575w511Fc8PEzgaG63S9hHx5Azw== + ui-auth-token: AgBgKRwErv5MtWIdqSN5BSQ/deHFE4469gzqL6gwp/3JidtBEiAAyuOIbc9dRCWuJNcdtixKOZT1rKuI8O/e6oJ6ABGAx+ckDfH468VKsR4AJhdZGcxub5j79NKccbNA5MKTnqxlU05zukUOkgw3wdsOyuQr3RnfL31GFbEVyDMej+Xr7Rs0fWrfqpasCi45/PEi8fkofMg45kGc+LMfDgovNtkKla2MAXwYlUk24SA+lgCFoG2DHB50GdTK2byCks4Px4J3jQqqHdLUfC+/gyqimPoVUsVGb5SY0xeNepKfTz7lHEkmDOB9R1wfv7pUaLMapc+eVrVN7reLHKOA47IEedDpnlLm/fyEfPe7ktkejc59tPyIyxBubrN+Vb/+6bDgi7xsXy/N5uD/2sPqWVgI2D7veWOJEjM+GYOYeb2iRbf7KlrokCXD/gRMiQBKqYyUdjWx40MjrhxzPArOa+cL0kb1CUgwuyjmqq5hNIUO2wo6/kkeNtUeT7r067LE5rtMoZG2EAfw/xrL1CI2F+3c72JzRd//aClt1MsSfPPPoFz0TlWSSc7qNDhYHuQf//G/Zd29Nk/Ac52HVwdkl2AXJE/GIFdwi+ypqK5mjmSUTn/JVE0ZaA3OpHOF/01meISuqBo+HuAWCV8sw6en0F8AF+DF8rw8t5hDdexScXep5QuJmm4CYgqThyjz2V5xME9oMmGA+Yiu/MJ4AlkLowijpXkKEWpEOBRpRRhu2q2szhZqmx1oKJw+qJ40CQ== + template: + metadata: + creationTimestamp: null + name: dot-ai-secrets + namespace: dot-ai diff --git a/secrets/eu/keycloak-credentials-sealed.yaml b/secrets/eu/keycloak-credentials-sealed.yaml index d5bc82a..28e8543 100644 --- a/secrets/eu/keycloak-credentials-sealed.yaml +++ b/secrets/eu/keycloak-credentials-sealed.yaml @@ -7,9 +7,9 @@ metadata: namespace: keycloak spec: encryptedData: - admin-password: AgCbGhJduOlTEmA9OLys+ZmuXLU5T1dmdm+fRlrNnPtvjpDCmfKYnOP4rbvBs+Kvf1Y1TZ8fbR2UTISg96pXfY/TsSoJlU024NpBWSZ4e7HxJgHtq0ZwaNi+ruw41JiQ01LrGJ23q34WaPVFV/VfCDaWMlcIJ4gfRb9+lb8jXB6/sr9zSVNWtfrPoYdECcXvr3sOIz9Y5D1eiV/uuDuDQqm+vKpZp8a3bvWO5sB/uQ6a+ECCI/NUFJHtEP8sGLsCGoJMrO1UotXCvKLpIflcxMwW3NlzHd2e7vEPRdelpc3tlT88wd33lQ2LuXMJpQuMI6eHIcVA72UtTqD5bePU1HVWb6d8Dlg6UdebUDIphuJ5rsV3uKYNMgOxn+mvBVXhu5xRBsyanwHoUIKcvvuXaN93YtXHk4khgmt6GNNFdLFEZ69wDB5tPLrlpepvKzNzFYwEIRngYNQlBT3rn8677XgBhUltoq4vYoEqYredVCrh5N7R+DrYD2OYmfsB4WWoMrmE1+Kn/HA/Nu6i5M3mAYXg3NP6Yv9dI5ynMQKVBtwGnFn/hDym1cixmxdQs2FQ3ZaD62zzcXQ0Sc2CuiYVtKg8MZjLCGQoV6ZG7UrnuZ0cyzKE2SGmOaWwFHkRysXoRZiQ/eBybUWwU4DXigczSJ3zBEg57pjEsvloHqAEcGjMxqV1VqXpty/6tdFKkRvCgO6pH5wIQA== - password: AgBeVrYU5CDY8yBA0huXHEJuxykggruULsQB/Q/2sKOZTGZ9KyF1WSB2qJXG9CPIuwBz1YR/hlg4ezF7dQRaWNJspLXeTQgAmjwvgsa3XrfQEEoSFbt9KuN2T28W1S32YbQQJOQHMiMjn8+6YtIWTVM/14MWMGIw8J54Js1cWMsOS4BDT7qe059YMbivxOuPi+J0duL28doowmy5A393804LjyBTzWynB1XqmeBd9V5dJWeQSRl7jpvORR+LKGm/JXF9dX97kyeews0o89qa7VnPqxb9ZGUXhSufMEC8qmm2tXgyj80HaP+PE2aZ0M69o7Xmn1FkEaIxQ8stTyepG4+c7iySbdeqIWPWf0LpgPlkdEuQ7hXAtyNd2TLnD0AxNBiN8bKl2DGLIxaOpoa2PvVHqU08fFQuyHGB3uCvAhgO69Pvb6q62Og2hQHgNM4WWAgu1h9K/F3R4DyzxC0JPYAjfw+XIZCiuYX/UYpjEFXIw4pFU/2SCDGbbPNMKFVxppuBU8h8m7/QTawkJZ0y9ivv0SuIG1z2ExjX73dWk+h1my732wyaoGV+os4R5io5O+aznyvFUsH23N+jNj6WzMSromdYxHKIdmBakd7FiNqsXFy0vm7+aY+tie9WnB/ysC9zN13UpHTCkG5mn2E0X+ctzL9RM7ylQMMesfmk5VCnzewEQloGtEby334CCc/HH1X8+Ws9AO6okEMGplVI53cI2rXwXncFis37qO9yMeV5EZ18wfjXeaKxouiv/zZRuY4wnaojqJKIh0wnbwcOK1f8 - postgres-password: AgBlBR9F4xBm5B31+dwptBjxH4LvjOBd/kDr5AJ2CRpjBZOsEqZa2jnFjUujlNFKfRDWDdzt3FQ6E7VW8Qt2hsrngDgRcIH6aPoZBOSZ0xVMpqLiY+Ek8YlZXH86Oh3+m4wUFNtcM9lZwPGU09/byC5W5zWhpmsypd+waXHF6VJZmweayupvbCwHOL2af58y3HXdl7uqX2/VyTR6mVZKcLVePO/QHNTMBlkPlfw+HKjDuluPcS3MirZU42k6E5OGo3v9XheF4HoiiY5Z76YkBI9k1shYzQYk2w48iPg7QV4heZxUDgwcrkRdJA/eEAUQejakTM4m2yx84ef1xzW2vnnkxz/OTO4KCZtcM7LddBOuO6TS9LSb7JEcfx8SJ7+oQTVhEk0e7nEAMGQQrcOE8TOX122ejBnMkhJra57VnHQ73Xb6fEjQy9dhz6pEnukagIvKKsZ1gZwJkY6qBfq4MtE7nGRCL5OFdXjEFwjdHG1pyR57sYqwvmX5ufE0YZkymdxdmFl/Wigh8uIeZAkfwYgH+8bWg/1Z5yLYzXxhx4S+YjOfLO4ncy2zILw8LnIZvnrOCWgogiiYOJM32E78jZadmZOGVVXbgRvOPLZpw8458Frwsk9oaXMjL7TEryXsH8U8NzVeUBOnnmW9Z6w25a755Jkb2AwnLYbfxMoHhJm7DTTERNM4ZM4umPjUEsXhlTRo/rJ8oE4El54sdSEHIuau0U+ZDbfE0V0y2YhVK1gx2iKeDdvPmGp2LGRURT6AQNc/a5c3/S9MXLEgVSiaSUO5 + admin-password: AgCPuUHRhzPEau3aH9UKq9gp+k5Pu+lPKizST30OWEOW9ozwdcHbq90eb3b5Sd+R/fRPkqta/8TrhZx3uzqNhVnMUSSwfcnxG14Nfq9yl/CAIuXBrPH1C+SghLWrGcv/CaKwT1arTk/ECjtz87vfpGXwqjjRc3rnH1fRjMTsIL6vwOLKIad6dXREzviUGoyxWp6U7A9QPUQqAApRY3zF5cSH603TYiSd8+EIEVViaAAQDg/2xaknbGsMi6M9Ei74IQIFoYRHarMnpkdn7AZnbX5u/bpIOmjMloUazVg8DhPHlc72QiRzqq95YFwfjMqJumOZwS/Fo4rQXoyKM5/aUe0j6zx0wgWk+MNHq7JUjdsJcGIrlgFW4Um9oDhZbYiexDmwG6cR/68fKLOoFpVx4gyZH7Is3+v6l/EYm+ssARU0e+28xA6ZVGm7YhsrgjLmPd3z3YmVafgFWpyEvIGkAKqHNmWD0txIZ3LWptISqwjcr7Yk63kPFjnuXS9t3nWOzSF/RXnLgM90ZSkoytZU+CwyjAHFf30qRWeMvnXB6Ry4g7Koz54jpoMj0Gk9Fqb0v60DgOd4yxhTbT91Z83x2SvKeucnbDB8IK4xMK+QNfKy20BbCHLGfY5kCdYxIUMNlB0LUsjDOnGjqCrAUZjkeHNI04jNvHYMItgP4/EgTU7NB1f/GsP6cwwdwbKmpHoFKGBPIPo5iMUKK51/U/da/1Zm3f9Qr1HrKQ== + password: AgACslplMgI3ZXPpx7BivIM8LIhNDD5j38wbzd4H6IKqZab/nTNpmz2Hc2hCD96H4ljhCIiw1mETzCWshu3vwvCVtJnH2pXiVY0nHpJcSH1C/HqBPOXWVEuEcm2EgvKnf8koKdtkHtAaT6egWrpvNEHCnapo1pa53qB1cZk0PbPQ+cIcedVTa6yF7VzFcOQp56mbtu8ez/iww+K/SdiXtp/IkVCaiOsC7j5ZUwZVmPB/w/nEjgPPETezy3OsOaAokelKq77EUfrj4mh2CEtK9bHBRkbmZkTc9pVkKC/SuWRud5S6xV/VcLRdy/EA/wuqj+Z/XF3hOqyHUpNblbntZ60lqwxoju0ZK9hgZWwZyNvzz04/gv0TkX3/W5JTxNwemJ32AjQ8dQZOz6gdkTsX68lT5P4YlR58Dxsb3WxQxG1ishkDlxGCBA2oQSyyIMFULETuu47V32N8FXOy5fgWHO5ZSer5yFjIIjgU7GyywiSesvZ1m9G7lB5EX56ay1djEmUPiKP0KDyb3QSgsaVbUEdTDi0bXXyDgoVBZEgQNS6vttd5b4qV/IGDqDB4g2hw5xLiff2vkF/h0QPGEz61O+FDyYP5m2rR37o5lD6yWJGI6cB/epoFTKVANPyyGdrIDKtVlgEE30w/fciq+FdDRZob7Yg4uzXi/kGvAYfBmi0Ek04EIU1FLdEq8p+5yZhDSBy4TWjTuH5JTvYlwHjFhnLrCHC/zCBSgkOPgUkAGNOHdTya2ElqEGSSfcRrrvMhnY6ZlZubhGFSdPSrZSXTauSo + postgres-password: AgB/qqkiqoQ1d5g+ZMF5Za0ZQ0kpD+6sh/ds9/uc5XBD4VkaX2+1EDrEcq776x/thbngR1Fi3HNBafRtflfptO64N0r3GBJiakVIT+fEdp7tJs6VKjqcP9HFAEObAVEdlcJzOm9h8iPvhi+kxGgkJYSP3ojJ/lfoWoMLtZjsOJtlQH8yw0uPd8xlgVukssxUzFKkT2RkihLwlK/Zn/8MX8BNJT3W9Li8ruK7pm0+qKqsPKcq0hpKHVRDyYpWubAmiGtj8+Oe3qSYoQxepIdUvtEgEXqHJ5DFggxYB4VzVErdKcyn2tK8JraofcOYpR+JM+8VRH/HWTp0/zNJzY8LCgN/V+KkPkCghVDryW+8ZS2FSP/bcqhPWVbQE3s9EblUny8htsOu2GikbbQcF4LzBKDhIJS6hc9SUbw5sfT7CtREcU7XCfq4VyX7dQgOdiGotO8OTmpXFZtceX9E8ClGTQ3V9tKZkzlpILkEip3PjbkkLshNTtJaRsFgRu37PDddUHaVczBoPKBelONC+KpKBQDoiTfB08BylNxdROsic2ag3upR5avOuqf0YDU6tDKBP3S0FPhPoZOmisUsiZNc8/QqV590gSUvqNBRDWWbqLtYsHmgeYRKDH02kiEzIBQuARryOjK8gOxmv0oo1SVdOd33Q1smU/W5YQNqtoVcqRQOtn0lR/cfQjtHJ3OSSNBZuvEx0BMUsLkH5Oud6nmnVITxT6gxLOS6K/BgV8WyUwgaO0r1sFdjiArCQcaDTjLHkAopYjgWsjEkV04XdjxssEJB template: metadata: creationTimestamp: null diff --git a/secrets/gitea-backup-s3-sealed.yaml b/secrets/gitea-backup-s3-sealed.yaml new file mode 100644 index 0000000..0e185e3 --- /dev/null +++ b/secrets/gitea-backup-s3-sealed.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: gitea-backup-s3 + namespace: gitea +spec: + encryptedData: + AWS_ACCESS_KEY_ID: AgByl2/ljFIHzTD0Vy7srQoXRfdJRLG+WukgeLMJeiJm9MOFJBNEkr5ju2DemDNdRcViXQLN3yxqT/L0fG0rz+kaPQtLeVFToqr58vokxDasHw4WVIUOosi6wE+yaI16H6vxvdV8dck09nHE3fdBcwctlvjqsY7mKvyx4tYKdGRDoeJ/C7shYoDTl4E/ZtsSRkfOQ4Ojm6M6FU10zn03OOKrzaOUczxnqbAyGNFrZvCUGG38QQVnyYm/2HLofQgheSSQx8p0w5IgPRRhBM3IAyCLGkyEA8qHXSO3J5Y2m1izAoU9RVsHAVUMVTfYEdtUMADkDcywKzi0bi0ehBxDs2PuC89Uz4s6rKQ/v8xU+jUf3KogrZxsbuCh1iFVO/NCOLvQhLY5wU/946wl1WmS76/HxkM/D9Iq+KF0VsP0pJAA+SIyJQ3Bh0a9GnKRsJjCfO8qX0M0WSXhOpjw+4DvBoe653mV7n+LOEjQy7LJURFaz1HzQColelhhlQ5tHCN8J1jtjscFNiHZqyzBBm226X3oxr0cAC6e/l53ohNkKS3NA5aM/wouBrscs1/CfmDYKxujLyGqontFRQc3rtCZ29829u/RmuwietIVGeu+ooCacSM73zDqGYKM7HRr/Y7QYxuW0TiSaJhYMZQqsC4uo7ebZhRa2bWbCTHiVCs3jDdpSRyPgEECOvnOJbkTsh0e02HtrUEx7HBjLZua9FD2sskr8C9XJQ== + AWS_SECRET_ACCESS_KEY: AgCYxgmto8ytLz8QMm25/nIqqezlWWennhbPSPMB/aDYR+zW45LvAZbjwVp6wwSR/U5iXwfdZg5/k8+8CzGAKDjxc3Nwygih3cUpqVBOl+uOzD3W3oNDsyQckhmNA4jidwIbJF6ecV8O+GVuU19+E4QrkHTIP9lN5pnhkfIR7nMRVj4jdcNahH2O75huadGQII4GG+rmnGX1012IAhknq20CiOCbby3a2yHaU3om0srO1TkW/67jioQX2IvgUh7jVl6c4r1Y6b+glwV4bHc9GecDqNEF6uj6uy8ChNh1khRfUYVysIQRM9m1pV/qlKiUW/wjDZOjoW88IAg4wl2MMOFQby27jVwQWSe+kUPwRMf7HSNWoq/DaE/z71cMsdeEnAXtQMGwNzOr4EGM1n/faPGDWkj306l1xjoXNO2hLCNX8BspSBxQDWWADBGClC6C1AQX0HlsZLV0G18VCEkjTwvPRmPqigxzHganxWiWM0q3DfGrc+JvnGFW0r7waoKI5vIzxwzCbb3I042+3z2vsvo7ZW2mez+eKgeD0MvhRW2SlBMiE62MGJQL2BvTew1iU0Xean+19WGZO7PPysnOH6kU098kTJ5GjQpxlI2C+w6QC18q8eQeIvVyd/7wH+k7RMCDC+No6MCcYhDlcQNbIir6JJ7vIOd3n5NKXdg7Sy3SnjkyDPTOjXTwyn2hHkATMzUxgn/0frNZYSsEMTuoNlOfcZLr7UbFv+Qlr49rkAEMo3deohfGiQSD + S3_BUCKET: AgA6ulIpP/DrYOQ7iqo7CSeaSj0L/+PXDPZ7SxPmdu/wrqbXw7nlxAyp+7QHqkUub5XgVhwrk3KPmqUPcECPDbHdt93+nlM3PVD0yPNkVaijncEPRVccGu/VhE6Nae5lXI9U3pnAVAXn+7z8iwRpF/vr1qIGyQwiizsKyfBQhvRSupzOvY8sypbItDjyjttxlwMRorGI94GObeUS89kSx/MB7BWZJMtUuRRSG6YwgH/XIkIWbo2p2LD90EhNtaT9rYa1PcGP9BVcgHf/9zVCI4+1LWbfmZSgobwAEKLhzZfhzCM2DhN31CsVWhpp0x0gYmspNbtQQvoosKmeBPBT+BLkTabAhjx00rvVX3J48Er3PaVAjw6JxT1KSdaUuCmcIzX3O8ys/8PNacaEgEiqmeuIPgID8YHSXSfs9RIUkjKBWGydjE90lMQPgnqOBkPWTd1BNRqHj60D2pFp0/h7+j8/OfBj7doDp9ECwcdQqjwzX4pNi7WQiGd+Ri0/7DK1xSAOL0lwgg8VSrqCOIdasAZRVQuHhuWwKMyhdQyQCr+zCOQ/bLQaPeF1m7tKxFfU4lNz5tRiC8AOQI5aHX2gRrkpfugD3G9qFFQMl9EPCdNeBh/ezVWxSxekWvQTuGJ2WnLD33BhsZVLKjXa+tHjD/BsQjQgdCiqv8J9gPgtngx/pAFf0NaQezVU4tBfaYD4tetcrZDz5UtW5tTHaF4= + S3_ENDPOINT: AgBfKbxdU9hZrxIpbM6b+hDthXQ+uYrjWHGmxSdjGvxgHB2P7E+HqblPAHjIpiAsGiPESV75ZKs5/BdEBOoZpbvneAuRgVLZ5mxkWiQ35q7sJpWaUg47icnlEPFoFj8oxYbi1NRAYB5hbc3AU0s11mw7wre0pRZu0pgSidHk/lyuSXOHKQzuhXxKYmV61LjMxCQGwDNwbiDNuSZyU7AZ2r+vr2W7Tzu6G+tJctwQd3HOnYbOLMV4tBv93nc7EkU4tbdvdIvkGHEmKf4r4F+nGvKZ3fZie1QKyQvG/4+i8OKqby9XJtviEEfBqfrk5qb1dNQlqCfA4ThQ11MmRiP8VoaUp/yoUHHYACNY9HLBp+N5Cgbbcxo044U1c8b97I6ZOZJ2waZ9XkrBpYPPXWJRKxLeNgYoJqn3yMZV/U561DO1jLZ2cwQXXaFrm1WT7VjcB0czdJHW3FcOg9lzYKMCCTTX+cD4M1oK992931eECQxBecrtlQYD+NlJng8ARm7myTACOZGYMQo2gjdM4ZBh9KqoCT2jrFC6E29YwfRAIXrhiWdZZxOW6Bu9Txt8FgxnIlSz9iZ1hvbfdvrSZTilJbAAULKFqLUgNpQbdgYHtGXQkzFHqYmbdZ0vJ6taIli7y+/Rz6xKcql8uJLxnuncLvLvXHxXl+rWeKrAMn+jPvnuCcdq6yVPsI0Nz/B4EQRL7Nzl9XYQxSybAJACrrCjgEuHsquoPpuznlGuk2scuakXdWOzMg6i/MEk + template: + metadata: + creationTimestamp: null + name: gitea-backup-s3 + namespace: gitea + type: Opaque diff --git a/secrets/gitea-credentials-sealed.yaml b/secrets/gitea-credentials-sealed.yaml new file mode 100644 index 0000000..e13a349 --- /dev/null +++ b/secrets/gitea-credentials-sealed.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: gitea-credentials + namespace: gitea +spec: + encryptedData: + key: AgAN7QteEqfzTrlCRR4Hi50nPJ6GGktFqOJIMgYvAL+es3ELUy9KLGtcyaC+Bf+b9lmG5+UgiCdz8jWbnVI7xEhDKXx8+3KzLw3nPAhYPO3j4XSi3FhbHwE0p5IGlZ0/XdXP5SRr/X0P6wQewapLSClGoya/ErY549ORfL0KgbcUrPE4ReSKbER6R/biRzSpkngxljf0rw4uruhNTlmBvnL/0nRSdZGtww7fLhcQceARutGihKHjOTkzmv0d39hHJA+4xNTi4PCbfOUJBkKwriy7NTi6AZkGRH2b06m+/IGfYYvTp/2gdtf/omS0ZhIoEVeRLhupiwkRAlclOSk3EXRsCcMiH8Gp5Mp4lUuc6yYHwM9ONAbq+P2OlUTjNDOntgH4Z6p02D3pIUf6nterSTuga7haGr5Ph0OCYBk/V6KkswwuTsSBAv7EI5TXrZoALp0xxXBXS459sc21UCgp2K5Xdhb3K92KwtcQDND7qAdIVZXAgf1wahm2svjRWhvjnRVLqChbb8MvkEQC+queHvQvIW+/5mCEb/hAEY/nAHJqTqQhsHnpPxzMhLvxN+7S2zvqSC48zSVAbKvwNjJcaiZ3s4dTjgCaryT307Gg4FI9bHixtk30fB9ZUkMu1cm51QC8ld5CBVKLMi8mHSezshiZ1n2PVYMiOivbghDZQxJ+uutZVOlrF65uAtgnQgmm9q/fTD7FWQ== + password: AgBdnjk041bBX2rwz+q1G6QDk+QrHfvuzoTo1pCRE1w7NLACFgGwzeSTS7gjPXwtjIkgganpZYgT9ZwWK8+gcT6Sae/CBTcDPgE6Y7FtfpG244wgp+mn891TDQXONW/OBAYk+706YDbHfqqz5AQxKAEorJ74RZuLTAo6kB85A/eveEnQqq4cByJaInz0eprB1A4hGfBLF3XArjiVdL7f4X33mR4scH80GozJiqUnG6ruVv5jcEBXnzySVrnCaq4VgmgJTu5Lsjz+AyH7niGTGFIsVBeOnVyeodKwgDk4N3HeSj4w9No3ileZELD5nLZh+/OqdQHw2niBz5/hPjDyXw/p96PfLM3CFygXfs/Au1DqdDf8ccZufhF9OK9CCQqlIgCWK3W7lhsdhcgK65XisRG+hh37Vcxke+A9ZoLGMjIT43KejTqwE8r3wRxPrmejBUcVHohiD7L60dFbcUp64hSBktcIGwEAWn89gmD7Ar1gCoCUmFCBCTVD8c5LQ2W2xeeZ1OrtDpMkOustzXUkzqkqiCB6TPMqDQ4J5FXCyoTuHrBnieLWaKL82kdUohF1+Jak3UGe0rSpOxBmoNC/17yVXyiwBGOUZZP+DJyvwiJu1jc7j3USQ0HRCcR7af96IcwGxge4yWbR268oI53yiJlwh46JFcc8RkxQQaq3Nh3RoiGJohqx/or8JOIFsXEtUW4WSLNQa2tZ4dxRI8F83JnBCo2W+zvL2yXKRP/LLMXfF7nAOuXPrYKrRcdnnRhP1cdDUSIQk8c6gzvKJrsEkt/j + secret: AgAR/5Tj79Rk4fgvPwvRU63q2xF0fokEe0/KWQG9CGiYWwut8wB8PlST5WEcKWIjwNfGpSsyHRQHywIVwDT3m+3/wxfQ5eOtlHIfMuglSymqeLHvj9tuM2EWabd4o9frKHHJigNojXS42JZ1jVSW+PeEfNrGFBARWWaDYSA5586/M8U5qi6UOBIkdyG+796wqqIeUM52d+/lwHHcETQOdhwa5/B4IrbRB33CSv0UHmfM+MeNtUWEnza5+8tm+1mPFyBJ2N9isXxMeyHXWutwOJVsAjEe+X35mC1C03eIsDjpmpjQ+hweUhinu6zse7CsIwcBDuq4/+1xd8xaQ/lmYc01FophuAcVRqDZIhlaAjCLI7gCfX2fliluVuaUbZ/A4DY8Q/psOteODeI0kVEMCaL13T1j6tZZWyDqDAB91xKFh2ne4DOOTwkZLP2StMt7rAENy/eMld+VNrfNliHJUVJGMoPTzCxPaZQxJfRYIbpE5uGGA2fWkteuX5VJRqZ/8JshdBAUVG1XOSXlvM1zlW9KlyvO3j3KhekKG7bY+NndIwFMNhujG3FGWCA+k3MYQfAks3FWhW+ZXA0vYZpnPOJuYO5tqqDO4GLsrdJc5GaXMC7kcey90PqkwzkwhpxX7/X414w3bHWoYttmFCcQwSyA+S029gSFBIleNTKkjpXsNl8qB2NDJR1lf2kufO016WP07PB1NK0hGjvI0isz0uHZILrAD0i55t858/EjWsD2gg7jK2ww69vVxi2c2o6k370YVv92qoZCayTuYsXF0Ay6 + username: AgAmO891YOV8ZtWTNjOF+ZA+6yG69Jdvozq49/XCQabuVXSqoESWA/V/6RPL8+up8dFa+c/u0J+s+Pu1cJL5zjZmJtXStOIuhoIGBTkWBPgHQ11hp/5apkR4nt0gWR3fWQtz1v/073WsaQGhEyKHB4ZsxAXbwEQ41f+TQzdkvxu2DT07Kc4O0jN+QsAl/YtPLk2YW2WaiY/Q7dDq5+LKAbKVRR4Aa03OuMVDapPGOEogu3wC8vl9lkYMhPBrIosq9ycqk4roenKbtmFSIkzytLOLRKSNEmPUSklDMrO8HEGSByASxJP10jgUZW9wY4lE/DmsjuS2CQsGLXhZAye2S3hgGyewnIA1ZfsnJTb7jSsnQ0BVxm6HL9lKy/1PqDzMuE0Ou+LHiIt5DLbCrDmn0F+n8jhNlpCbtiZXVL/Tvo7SAruBe858CYXc2StFHNi5hA1dRP2CCGklY2Wmi8u5JeOdi8RXdhw8deZtI0G+I1NltqzhXdDR55kMWahy2BTImJrGj8owrdg5bFz1Ne63P7jiS80BeUfDKffYAWuslDEiKkhO8YSoVj7ixeWIfXvZRyaecaCMcZ+VuNlZy3R5CbFTcIyZkfotK0+aA9S/q+iM6K36ghTxpQv6WSecnHpeR8mkVckCGVo+iiCPIFfQw2VwOHn+BtvbH8pfr0McKoGHAvoDGGH9sjgDbjAGwCYFYr7qHTKaFc1vbVcUNw== + template: + metadata: + creationTimestamp: null + name: gitea-credentials + namespace: gitea diff --git a/secrets/gitea-runner-token-sealed.yaml b/secrets/gitea-runner-token-sealed.yaml new file mode 100644 index 0000000..41e2594 --- /dev/null +++ b/secrets/gitea-runner-token-sealed.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: gitea-runner-token + namespace: gitea +spec: + encryptedData: + token: AgBZkU9AgXmFoBURd4WImEFFY4k3LTvGmMJ0ZEZETYzQNS9ICyM0TZfK9nF/bSY48y9t2Vd+VNNW2WJhqec05HE9rLe/vQmo2L8OlBLbCpFL9vCEFJ/h8AiWdHGsLJtCH6tOJwBQ667KPfAGFeYZzKf1XF5HA8O6G1+v7kO3mtSNSIYWq7S/swe5QO+bgtclkeDjg3xgOhjYTzlOsg+rcTknhwGa2YpL22rFN6Tbf94oTK6xVBYrkly1nhiALGYyGnROSv/Ua8+M4xEF5tr0QslwjRmzaQTC+Skt1HKXogsWVzE03RZHWQpmcz1ltaKfikl8WQwBmOBDeg9xNRfBu5n5jli9NvdKZgMEIyQQZtp27QCsIk+FLxbAb2Zy/hG3G93Jvk9mRzgryXyi0te8w+iEP1WtVOYHSyLmk0AMCahn2hPIfzQvlOXJxoSFhLbwm5eSwtqYTQkQ2WFKmwaznSZBUmk1a+LvxV5pOhPMvc52w0v8u13ng/YzDDnyjY/6WdYX5pJtYOGUFDt79yd2Bd0Hc4CH9y50dy84YfdlC/prgTVeVfoJFW48qhE4sNkBhL28PjgkcYmIOb9fskP1o3RuHIoR28y9ipZMkzdLimCRzIQxf4GmdOlGsoEEvujgiGr1ta8ilRoAjjDAD593PuYELFRfGYjk4KGNkz6MP3t4UDRlG8umXMQIqhEWUaRGytqMnfrPGiGPAGCBfAAes3mooDNO/NO+WwNq09fd1OWFeDvzJMOLxWLy + template: + metadata: + creationTimestamp: null + name: gitea-runner-token + namespace: gitea diff --git a/secrets/gitea-smtp-secret-sealed.yaml b/secrets/gitea-smtp-secret-sealed.yaml new file mode 100644 index 0000000..3fecd0b --- /dev/null +++ b/secrets/gitea-smtp-secret-sealed.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: gitea-smtp-secret + namespace: gitea +spec: + encryptedData: + password: AgBMuISQeA41mtBIOo686sND2EO12Jv7BIogL5G7xxt7wKfIk88dQrU74vs+fv3OtPce2Ra63QkR6po31M9fZkoiYba5yqJtEOc6em3y0ZxM/UzavbRvHwrvsWEYqmHBnkCUjcJijdGbSX+rHyGsXLfZO0gOjqXO379Zl1fzmV4p3F5REXFQm6HorVtWX/LOSRj9GDW76l6KgPR/A+CbtAx8Cq0m3D3fyiaVLEReP3uOgBOo70APHj9Yp56EcgOtVa1pEgCxR4ctXeS4t/EliWcHc/JT4TBdRBRDYPKLfME6FvJxjLjaSZcWxtJrJzCv3+vA5LlfObuHY31aSDRqYwO4VBCPhf3Aa6Z5UXgUnmAtJRhHa9pKSSjW48jgNb1jDPIkQn5XgB2/twJ+gX3inAkrTQ82JJ75Rz7XWC8KmYkOtkgXgU2buCa4nIfPeXOr5qvutyywxV1Ge1nK0fQYneQZVFXlHTbAQXBJMpVvJoJ+G3xGjm1904/iBGkVKmNrQwaABUsGBC6ZIHGOTa45GBqrg3ODU2Gr61SCYxv6m3pMU1msR7QYne0oqLCVD8mLDaeSeiQI4ZY9u4ddsVwM6l2BFrT6+3IQuYPBgOoodzDVlCgmA7hoekhpak9vZ0loSHaWDXdNt75SemAjsQfwCO5sSEkr+wbCJEQpXh5p38RMZKTuOh3nYEGQEx/MQNl3VD4FarK/zOJM9EO9IkqdM4LnqVo3zPX4KAPosS1PPKS8 + username: AgBF6MiaI1x2xQOUoF4NUh4MeFF64Db3vywcEO0FdJ0U9EirVFMsBSSiqJLy8ok43ha72s+/RLBNHiSSRKX1UMWwwCsfs+LQJNh9EetgHRxoyqkHiqRMX5V2acU2scdPE/FCFQFOYzAjweup+kP8xNu1WKuDtPBRiAgBNDfW59ihFi9TgOJQ2AnDHottjm5CNaWsbTOSgZrXqzCEChfHu5K0W9cty9ENHYqnYDfcm/zPLYeUPW0gVN5GJq3lPo9vZjM7T2JnwryjOkBaPCRCzHOpRF3bMrArFrjbUlH6gdI0APf4CzGLMMKol/jTMG2tLBseaNQHfbz6p1vFYExCL60gSN46fzh10zIWaIC2O+SgoLLOizkQZWf/v86cdRerBSl6PFmbRUO18XUQ4SyR/WPM71HD1jeLnUZjKtkOu+fqQKlv8kBSELHGqiURNYDnbmUA1LQpdNkDnMkRS+uzQ3XwWCSQBAn+u9wh69kg1oPVEN60Nc4KpNwFIg25aycGkP3cMklfl3/u9nr5KruwtJe+hl2ynSk8zeEFWWQrBki7+88CH9aWVW/GTA8Ho7Fz+gp4ZUdUA0WhH2LRAQIN945pvJIkHm/AYAhH6pZiXdBzYeguPY5VEf6hDVM1sa39aSZzs81cj0YHxbjR/BoBxbUAa9xW7JYH2rcIqXDhJB4zq2H++8e0eABsdQC3tMmE1eQA51d0yg8+2fX+CRkcvMCmI3VjS/mdirrtnctv + template: + metadata: + creationTimestamp: null + labels: + allowedToBeCloned: "true" + name: gitea-smtp-secret + namespace: gitea + type: Opaque diff --git a/secrets/musicman-auth-oidc-sealed.yaml b/secrets/musicman-auth-oidc-sealed.yaml new file mode 100644 index 0000000..ba36df7 --- /dev/null +++ b/secrets/musicman-auth-oidc-sealed.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: auth-oidc + namespace: music-man +spec: + encryptedData: + client-secret: AgAKQ24qP43Dopfb6LfH0o7RIQjK3IMjII8knmdpfL5ldXJRGaYYYQDjZJcIE5HiLoEFXKkLHm39k83l6sJ1fn1RCbbskSGmosYNenJgxOqTZdlyC911km1PVMu890N2JEqUVRQb4YTn8+1dYUglWPL+zaAki3wJ84G5uM6ZY0R7mMEp/5J8RFEeByzdTiHLtB9U2fPnBJiGdKfFN/ysnoZOLN0nAHvvnTEa4pkO2BXNrrhVl7Nje/y9IDmQntCNIHomF9Fy4bo1SpChj1vRXWbLETlMdFkRINHMVWAvemoIDv+CaHVRaaoy/XtTngqA5lyZZwiwiix+HBIDPE5HAnGE/Zqgc2L+ByR3ak33H6O6iGJoVdR79uu9PiW0iaWJwruXgSfm7Fq0KuOHNg0QzfTqsZf0jgT5RhT4apSGHZOl2g4QUkMUee6BPuFM435ZZFdOfDZw1YRdqdFvI6OJgx7+G+kwwnEa125LuBgbJzqLRVxH0LbD9HoBoEqZUB4H0V+0PUY7uVSKcpZUi0RwiLjcoHJzaJlgJdaEc9GrDhh8OvDXOxsBJcRrJDmAH5WbGLXbq7+HUknfbJFkcl6rg9bPWDah0DDf98weqUdafpn0TTt7qoOGW1z27yVApGn9bm9B7ykCa/i9QtX/6G9RTaoslTU45pYzA1Clw9ccRNxv7O+2afU5KZORZPYNXCho4K7tpR7Hw788Rj68x1GYxrZh82+s0fs/SYpvzvN6EiFxKY8R+f1azefD + cookie-secret: AgAPJomNWVV2m8izVg8CQ78mnXP9qQaXh1WaIELv4JzwyUIcMOddDDq8PWzmAiWfV24iYj/oy9QB0jruI8+78H5nU6QsF5KB3DlZiDjzwaUeNOs1q4wPsQ/aU9JygYpWD2dYUn4pBYqQQxJyf/gJ1u6jbGMbjsO5J3kkK4RaFiTNt+A3151nmYVgWToknTPPcTaFgpH0xumhQHicO9MyaNsSKOJ1c2AeUiWHqc/0a39tuchCgq4Gm1FOKjXXfevcZw/IamnRiEegoAr5qgSPl7e1xfZeG49FJacsfpWEuQhmKGsaePTHok141VnkKHhlmFdewe3Sn+kSWJbtAx6jky0sqClUKqopVjxfj5IKwcucE0NjxxX2HXmQb/e9nY3BLQ1CRjQQVCNa+JJy6DvAbcERHs7t9T7ATXm4S5lawp9fp8UB1ySy++q4Qvmc+4/16tgugqVEths/n+gSYLWAM8Ibd42RhNXlWqjzvpAUXmHX3Dt/26wNy+5ZnRlcwOIotPBdDqQKlp3fGKc2loC2JHcyN1PH31EwY5+h4pdU/sGSU+EsPd43hALUHq3XzvibHVDQTzyerJgC77NX6kvu2W+V5iWN9LYAAVjMh5yzLYjfgoC/zzIiOIe9yRf6zJzW0nhSkdcoC8EH0lcwEEDp0d/CuO0A6AwWOBaer+tnQtjHeZse9RdKEfyisJDDa7xUNj4T9e8Z+shdkU94vladQpVP1lyRd4auXF7B+T0+1Pkmvac8wDHDk67iomi+1BpS+/2jdJUGJO+91+qIdCFLPi1Y + template: + metadata: + creationTimestamp: null + name: auth-oidc + namespace: music-man diff --git a/secrets/renovate-env-sealed.yaml b/secrets/renovate-env-sealed.yaml new file mode 100644 index 0000000..7ba928c --- /dev/null +++ b/secrets/renovate-env-sealed.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: renovate-env + namespace: renovate +spec: + encryptedData: + RENOVATE_GITHUB_COM_TOKEN: AgBliRXcjCrWeZKa3A0RuQatvluoF30R8HEAt7aVJwSf9EbMtB/ozbtsbHxYTsyfpdnaewD+BMRbaDdQOTpmlPxfK5eqKFuLXJGCXmiiMEHvK3GG0y0STjfgC2GyrXvtmh/lg/Awod9ZneJ2+Xoi0X7ZIngWwHXmgvsRfczKtwZAZm/YVDLGHRW2BCwBgzStZquopxYGQRa58WSx9XkE/cV8UOb6Kw+4fGMKggJPr8ERUQyermCZ0HBOpkQwPpWoPhRFdCMj2qasVLttkK/jLG2romy3JZ59RGrQ7bj2p9IdJujqtteWb4vDdOc4jvplKWlB8QptZ872a2MPeyvopqkCsmnsfi6tup6uBaUzE8Hym0olpqK73dCjc4RP7x64KwsdGOyi2aJgEz1/Ro7GMu7sam5sdpTjpdA3EbyC8tZrnlC51mUlq4ghP5XELGj6DjvLWZ3dk0UOSQ4vQ/KLGyA29VBgvV3MdabpGNhs5VlU69p4RofxoFT9n9CrtJN8FPOnMUK6L58Zf3w+eYJQMuyttgf/h/stQUx6OU7R4d/4xA1j4YCFRxw2mXaYO9Tjsxsq2KB1v3TzyZKtZfYLeiNhv/rAfPs7krACs1ncNXVtS7sP3G14+Asvu6EdnlM86LThBmIeKRW8lSry5pO3IMOIzNGrqulFnuTGP9fy9cs5d58q/E4sUXoY0Liiod92+1RO8dpyn+xf9afCAei8lG6TFNPWdLzcl8WOJgZLWmRdRNhezOmSZHxnh8P/Ul2oxhrVZSYGLhvIfBVU9oY+s7lgw8FpM08RLdP3OsKigs5/uiE/56qHZHvj2YZf6mg= + RENOVATE_TOKEN: AgCV8DmDz1CT+wjtghW9ERy9tY/GCZfehPbd3vqikGx8tsZzxEoKukJzkeRJmsHqDTXylFMZHB+saYuk+vWOeMT7oGs/6wIC7eC08GN4OcBYpihrTSrzoAkp1WwLJV0KoNGbtbgoQXdlPJOWBjs6X90mrFmpwRewcaADks/uS/ONZw3MhiSbukYN3L45uL6b6gUp1Yl4nDyCyvCDoRpr1FQt2MMltyChimUtLXoMrnV3iO0oFlFMITJRjvauYrKXdD0xyWxji3Rbrowbgr/iFkz/0fTpg6AwhP/ISRWtO+NDB3kZ6/CvcK2a1phTIgf2eiHfvMQIwnpfRauyUPtNHkCInC7OOtn0eyHZmAqkSaJTN1rJN3X8lRIgjhtZr9upE6ZCLK5kMdq87oDjtvwc0X0AyeTxV3EGXh87Zfda97KhG5LSC3uShzc8AYPA7SGxMTuoF4wON0Qa7AdAeGvtL+2GuhRkHhmyOeY4iORrVo8TlJsN1DsZmrbRArJWzvMcb31gefxTmg8tSZhuLOo4RHsUayCNIPmW/AvpcbGJS1pPXS8nPkDqSoeL6UW/G8CoEEisr50+9ztGOCmurobbadHcs4LhwAC2s9SuYuCTTSK4c98ke901ks4Iho8TuG3WmBkgJlEsNMigMsnW6pwlx/Xnij06csRY8Xcfh77MjluQXx833oKRxffwC9NuIgXcBG0cqGraEF6RFxKlu3DsWRIElt5Rnju/WSNhDsWubKrMVtKXuYX0tTBn + template: + metadata: + creationTimestamp: null + labels: + allowedToBeCloned: "true" + name: renovate-env + namespace: renovate + type: Opaque