From 9e3c7e7a3f9a4392e2323716398feeb588aa1068 Mon Sep 17 00:00:00 2001 From: ZhangNing10 <135692613+ZhangNing10@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:14:28 +0800 Subject: [PATCH] adjust env parts (#195) * adjust env parts * adjust comment --- HelmSetup.md | 3 +- charts/devlake/charts/grafana-6.56.6.tgz | Bin 38882 -> 0 bytes charts/devlake/charts/grafana/.helmignore | 23 + charts/devlake/charts/grafana/Chart.yaml | 29 + charts/devlake/charts/grafana/README.md | 685 +++++++++ .../charts/grafana/ci/default-values.yaml | 1 + .../grafana/ci/with-affinity-values.yaml | 16 + .../ci/with-dashboard-json-values.yaml | 53 + .../grafana/ci/with-dashboard-values.yaml | 19 + .../ci/with-extraconfigmapmounts-values.yaml | 7 + .../ci/with-image-renderer-values.yaml | 19 + .../charts/grafana/ci/with-persistence.yaml | 3 + .../grafana/dashboards/custom-dashboard.json | 1 + .../charts/grafana/templates/NOTES.txt | 55 + .../charts/grafana/templates/_helpers.tpl | 201 +++ .../devlake/charts/grafana/templates/_pod.tpl | 1159 +++++++++++++++ .../charts/grafana/templates/clusterrole.yaml | 25 + .../grafana/templates/clusterrolebinding.yaml | 24 + .../configmap-dashboard-provider.yaml | 29 + .../charts/grafana/templates/configmap.yaml | 138 ++ .../templates/dashboards-json-configmap.yaml | 35 + .../charts/grafana/templates/deployment.yaml | 51 + .../grafana/templates/extra-manifests.yaml | 4 + .../grafana/templates/headless-service.yaml | 22 + .../devlake/charts/grafana/templates/hpa.yaml | 52 + .../templates/image-renderer-deployment.yaml | 130 ++ .../grafana/templates/image-renderer-hpa.yaml | 47 + .../image-renderer-network-policy.yaml | 79 ++ .../templates/image-renderer-service.yaml | 31 + .../image-renderer-servicemonitor.yaml | 48 + .../charts/grafana/templates/ingress.yaml | 78 ++ .../grafana/templates/networkpolicy.yaml | 52 + .../templates/poddisruptionbudget.yaml | 22 + .../grafana/templates/podsecuritypolicy.yaml | 49 + .../devlake/charts/grafana/templates/pvc.yaml | 36 + .../charts/grafana/templates/role.yaml | 32 + .../charts/grafana/templates/rolebinding.yaml | 25 + .../charts/grafana/templates/secret-env.yaml | 14 + .../charts/grafana/templates/secret.yaml | 26 + .../charts/grafana/templates/service.yaml | 55 + .../grafana/templates/serviceaccount.yaml | 17 + .../grafana/templates/servicemonitor.yaml | 48 + .../charts/grafana/templates/statefulset.yaml | 56 + .../templates/tests/test-configmap.yaml | 20 + .../tests/test-podsecuritypolicy.yaml | 32 + .../grafana/templates/tests/test-role.yaml | 17 + .../templates/tests/test-rolebinding.yaml | 20 + .../templates/tests/test-serviceaccount.yaml | 12 + .../charts/grafana/templates/tests/test.yaml | 49 + charts/devlake/charts/grafana/values.yaml | 1240 +++++++++++++++++ charts/devlake/templates/_helpers.tpl | 2 +- charts/devlake/templates/deployments.yaml | 17 +- charts/devlake/templates/statefulsets.yaml | 2 +- charts/devlake/templates/validate.yaml | 2 +- charts/devlake/values.yaml | 32 +- 55 files changed, 4916 insertions(+), 28 deletions(-) delete mode 100644 charts/devlake/charts/grafana-6.56.6.tgz create mode 100644 charts/devlake/charts/grafana/.helmignore create mode 100644 charts/devlake/charts/grafana/Chart.yaml create mode 100644 charts/devlake/charts/grafana/README.md create mode 100644 charts/devlake/charts/grafana/ci/default-values.yaml create mode 100644 charts/devlake/charts/grafana/ci/with-affinity-values.yaml create mode 100644 charts/devlake/charts/grafana/ci/with-dashboard-json-values.yaml create mode 100644 charts/devlake/charts/grafana/ci/with-dashboard-values.yaml create mode 100644 charts/devlake/charts/grafana/ci/with-extraconfigmapmounts-values.yaml create mode 100644 charts/devlake/charts/grafana/ci/with-image-renderer-values.yaml create mode 100644 charts/devlake/charts/grafana/ci/with-persistence.yaml create mode 100644 charts/devlake/charts/grafana/dashboards/custom-dashboard.json create mode 100644 charts/devlake/charts/grafana/templates/NOTES.txt create mode 100644 charts/devlake/charts/grafana/templates/_helpers.tpl create mode 100644 charts/devlake/charts/grafana/templates/_pod.tpl create mode 100644 charts/devlake/charts/grafana/templates/clusterrole.yaml create mode 100644 charts/devlake/charts/grafana/templates/clusterrolebinding.yaml create mode 100644 charts/devlake/charts/grafana/templates/configmap-dashboard-provider.yaml create mode 100644 charts/devlake/charts/grafana/templates/configmap.yaml create mode 100644 charts/devlake/charts/grafana/templates/dashboards-json-configmap.yaml create mode 100644 charts/devlake/charts/grafana/templates/deployment.yaml create mode 100644 charts/devlake/charts/grafana/templates/extra-manifests.yaml create mode 100644 charts/devlake/charts/grafana/templates/headless-service.yaml create mode 100644 charts/devlake/charts/grafana/templates/hpa.yaml create mode 100644 charts/devlake/charts/grafana/templates/image-renderer-deployment.yaml create mode 100644 charts/devlake/charts/grafana/templates/image-renderer-hpa.yaml create mode 100644 charts/devlake/charts/grafana/templates/image-renderer-network-policy.yaml create mode 100644 charts/devlake/charts/grafana/templates/image-renderer-service.yaml create mode 100644 charts/devlake/charts/grafana/templates/image-renderer-servicemonitor.yaml create mode 100644 charts/devlake/charts/grafana/templates/ingress.yaml create mode 100644 charts/devlake/charts/grafana/templates/networkpolicy.yaml create mode 100644 charts/devlake/charts/grafana/templates/poddisruptionbudget.yaml create mode 100644 charts/devlake/charts/grafana/templates/podsecuritypolicy.yaml create mode 100644 charts/devlake/charts/grafana/templates/pvc.yaml create mode 100644 charts/devlake/charts/grafana/templates/role.yaml create mode 100644 charts/devlake/charts/grafana/templates/rolebinding.yaml create mode 100644 charts/devlake/charts/grafana/templates/secret-env.yaml create mode 100644 charts/devlake/charts/grafana/templates/secret.yaml create mode 100644 charts/devlake/charts/grafana/templates/service.yaml create mode 100644 charts/devlake/charts/grafana/templates/serviceaccount.yaml create mode 100644 charts/devlake/charts/grafana/templates/servicemonitor.yaml create mode 100644 charts/devlake/charts/grafana/templates/statefulset.yaml create mode 100644 charts/devlake/charts/grafana/templates/tests/test-configmap.yaml create mode 100644 charts/devlake/charts/grafana/templates/tests/test-podsecuritypolicy.yaml create mode 100644 charts/devlake/charts/grafana/templates/tests/test-role.yaml create mode 100644 charts/devlake/charts/grafana/templates/tests/test-rolebinding.yaml create mode 100644 charts/devlake/charts/grafana/templates/tests/test-serviceaccount.yaml create mode 100644 charts/devlake/charts/grafana/templates/tests/test.yaml create mode 100644 charts/devlake/charts/grafana/values.yaml diff --git a/HelmSetup.md b/HelmSetup.md index baf3fd4..794513e 100644 --- a/HelmSetup.md +++ b/HelmSetup.md @@ -136,7 +136,7 @@ Some useful parameters for the chart, you could also check them in values.yaml | ----------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------ | | replicaCount | Replica Count for devlake, currently not used | 1 | | imageTag | The version tag for all images | see Values.yaml | -| envs | The common envs for all pods | {TZ: "UTC"} | +| commonEnvs | The common envs for all pods except grafana | {TZ: "UTC"} | | mysql.useExternal | If use external mysql server, set true | false | | mysql.externalServer | External mysql server address | 127.0.0.1 | | mysql.externalPort | External mysql server port | 3306 | @@ -161,6 +161,7 @@ Some useful parameters for the chart, you could also check them in values.yaml | lake.image.pullPolicy | pullPolicy for lake's image | Always | | lake.port | the port of devlake backend | 8080 | | lake.envs | initial envs for lake | see Values.yaml | +| lake.extraEnvsFromSecret | existing secret name of extra envs | "" | | lake.encryptionSecret.secretName | the k8s secret name for ENCRYPTION_SECRET | "" | | lake.encryptionSecret.secret | the secret for ENCRYPTION_SECRET | "" | | lake.encryptionSecret.autoCreateSecret | whether let the helm chart create the secret | true | diff --git a/charts/devlake/charts/grafana-6.56.6.tgz b/charts/devlake/charts/grafana-6.56.6.tgz deleted file mode 100644 index 956cabb9cca16d442e0b1fc69b37da4e9f429fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38882 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 GqyqqQgnPyS diff --git a/charts/devlake/charts/grafana/.helmignore b/charts/devlake/charts/grafana/.helmignore new file mode 100644 index 0000000..8cade13 --- /dev/null +++ b/charts/devlake/charts/grafana/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.vscode +.project +.idea/ +*.tmproj +OWNERS diff --git a/charts/devlake/charts/grafana/Chart.yaml b/charts/devlake/charts/grafana/Chart.yaml new file mode 100644 index 0000000..6360b4c --- /dev/null +++ b/charts/devlake/charts/grafana/Chart.yaml @@ -0,0 +1,29 @@ +annotations: + artifacthub.io/links: | + - name: Chart Source + url: https://github.com/grafana/helm-charts + - name: Upstream Project + url: https://github.com/grafana/grafana +apiVersion: v2 +appVersion: 9.5.2 +description: The leading tool for querying and visualizing time series and metrics. +home: https://grafana.net +icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png +kubeVersion: ^1.8.0-0 +maintainers: +- email: zanhsieh@gmail.com + name: zanhsieh +- email: rluckie@cisco.com + name: rtluckie +- email: maor.friedman@redhat.com + name: maorfr +- email: miroslav.hadzhiev@gmail.com + name: Xtigyro +- email: mail@torstenwalter.de + name: torstenwalter +name: grafana +sources: +- https://github.com/grafana/grafana +- https://github.com/grafana/helm-charts +type: application +version: 6.56.6 diff --git a/charts/devlake/charts/grafana/README.md b/charts/devlake/charts/grafana/README.md new file mode 100644 index 0000000..644b43f --- /dev/null +++ b/charts/devlake/charts/grafana/README.md @@ -0,0 +1,685 @@ +# Grafana Helm Chart + +* Installs the web dashboarding system [Grafana](http://grafana.org/) + +## Get Repo Info + +```console +helm repo add grafana https://grafana.github.io/helm-charts +helm repo update +``` + +_See [helm repo](https://helm.sh/docs/helm/helm_repo/) for command documentation._ + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```console +helm install my-release grafana/grafana +``` + +## Uninstalling the Chart + +To uninstall/delete the my-release deployment: + +```console +helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Upgrading an existing Release to a new major version + +A major chart version change (like v1.2.3 -> v2.0.0) indicates that there is an +incompatible breaking change needing manual actions. + +### To 4.0.0 (And 3.12.1) + +This version requires Helm >= 2.12.0. + +### To 5.0.0 + +You have to add --force to your helm upgrade command as the labels of the chart have changed. + +### To 6.0.0 + +This version requires Helm >= 3.1.0. + +## Configuration + +| Parameter | Description | Default | +|-------------------------------------------|-----------------------------------------------|---------------------------------------------------------| +| `replicas` | Number of nodes | `1` | +| `podDisruptionBudget.minAvailable` | Pod disruption minimum available | `nil` | +| `podDisruptionBudget.maxUnavailable` | Pod disruption maximum unavailable | `nil` | +| `deploymentStrategy` | Deployment strategy | `{ "type": "RollingUpdate" }` | +| `livenessProbe` | Liveness Probe settings | `{ "httpGet": { "path": "/api/health", "port": 3000 } "initialDelaySeconds": 60, "timeoutSeconds": 30, "failureThreshold": 10 }` | +| `readinessProbe` | Readiness Probe settings | `{ "httpGet": { "path": "/api/health", "port": 3000 } }`| +| `securityContext` | Deployment securityContext | `{"runAsUser": 472, "runAsGroup": 472, "fsGroup": 472}` | +| `priorityClassName` | Name of Priority Class to assign pods | `nil` | +| `image.repository` | Image repository | `grafana/grafana` | +| `image.tag` | Overrides the Grafana image tag whose default is the chart appVersion (`Must be >= 5.0.0`) | `` | +| `image.sha` | Image sha (optional) | `` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.pullSecrets` | Image pull secrets (can be templated) | `[]` | +| `service.enabled` | Enable grafana service | `true` | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Kubernetes port where service is exposed | `80` | +| `service.portName` | Name of the port on the service | `service` | +| `service.appProtocol` | Adds the appProtocol field to the service | `` | +| `service.targetPort` | Internal service is port | `3000` | +| `service.nodePort` | Kubernetes service nodePort | `nil` | +| `service.annotations` | Service annotations (can be templated) | `{}` | +| `service.labels` | Custom labels | `{}` | +| `service.clusterIP` | internal cluster service IP | `nil` | +| `service.loadBalancerIP` | IP address to assign to load balancer (if supported) | `nil` | +| `service.loadBalancerSourceRanges` | list of IP CIDRs allowed access to lb (if supported) | `[]` | +| `service.externalIPs` | service external IP addresses | `[]` | +| `headlessService` | Create a headless service | `false` | +| `extraExposePorts` | Additional service ports for sidecar containers| `[]` | +| `hostAliases` | adds rules to the pod's /etc/hosts | `[]` | +| `ingress.enabled` | Enables Ingress | `false` | +| `ingress.annotations` | Ingress annotations (values are templated) | `{}` | +| `ingress.labels` | Custom labels | `{}` | +| `ingress.path` | Ingress accepted path | `/` | +| `ingress.pathType` | Ingress type of path | `Prefix` | +| `ingress.hosts` | Ingress accepted hostnames | `["chart-example.local"]` | +| `ingress.extraPaths` | Ingress extra paths to prepend to every host configuration. Useful when configuring [custom actions with AWS ALB Ingress Controller](https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/ingress/annotation/#actions). Requires `ingress.hosts` to have one or more host entries. | `[]` | +| `ingress.tls` | Ingress TLS configuration | `[]` | +| `ingress.ingressClassName` | Ingress Class Name. MAY be required for Kubernetes versions >= 1.18 | `""` | +| `resources` | CPU/Memory resource requests/limits | `{}` | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `tolerations` | Toleration labels for pod assignment | `[]` | +| `affinity` | Affinity settings for pod assignment | `{}` | +| `extraInitContainers` | Init containers to add to the grafana pod | `{}` | +| `extraContainers` | Sidecar containers to add to the grafana pod | `""` | +| `extraContainerVolumes` | Volumes that can be mounted in sidecar containers | `[]` | +| `extraLabels` | Custom labels for all manifests | `{}` | +| `schedulerName` | Name of the k8s scheduler (other than default) | `nil` | +| `persistence.enabled` | Use persistent volume to store data | `false` | +| `persistence.type` | Type of persistence (`pvc` or `statefulset`) | `pvc` | +| `persistence.size` | Size of persistent volume claim | `10Gi` | +| `persistence.existingClaim` | Use an existing PVC to persist data (can be templated) | `nil` | +| `persistence.storageClassName` | Type of persistent volume claim | `nil` | +| `persistence.accessModes` | Persistence access modes | `[ReadWriteOnce]` | +| `persistence.annotations` | PersistentVolumeClaim annotations | `{}` | +| `persistence.finalizers` | PersistentVolumeClaim finalizers | `[ "kubernetes.io/pvc-protection" ]` | +| `persistence.extraPvcLabels` | Extra labels to apply to a PVC. | `{}` | +| `persistence.subPath` | Mount a sub dir of the persistent volume (can be templated) | `nil` | +| `persistence.inMemory.enabled` | If persistence is not enabled, whether to mount the local storage in-memory to improve performance | `false` | +| `persistence.inMemory.sizeLimit` | SizeLimit for the in-memory local storage | `nil` | +| `initChownData.enabled` | If false, don't reset data ownership at startup | true | +| `initChownData.image.repository` | init-chown-data container image repository | `busybox` | +| `initChownData.image.tag` | init-chown-data container image tag | `1.31.1` | +| `initChownData.image.sha` | init-chown-data container image sha (optional)| `""` | +| `initChownData.image.pullPolicy` | init-chown-data container image pull policy | `IfNotPresent` | +| `initChownData.resources` | init-chown-data pod resource requests & limits | `{}` | +| `schedulerName` | Alternate scheduler name | `nil` | +| `env` | Extra environment variables passed to pods | `{}` | +| `envValueFrom` | Environment variables from alternate sources. See the API docs on [EnvVarSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#envvarsource-v1-core) for format details. Can be templated | `{}` | +| `envFromSecret` | Name of a Kubernetes secret (must be manually created in the same namespace) containing values to be added to the environment. Can be templated | `""` | +| `envFromSecrets` | List of Kubernetes secrets (must be manually created in the same namespace) containing values to be added to the environment. Can be templated | `[]` | +| `envFromConfigMaps` | List of Kubernetes ConfigMaps (must be manually created in the same namespace) containing values to be added to the environment. Can be templated | `[]` | +| `envRenderSecret` | Sensible environment variables passed to pods and stored as secret | `{}` | +| `enableServiceLinks` | Inject Kubernetes services as environment variables. | `true` | +| `extraSecretMounts` | Additional grafana server secret mounts | `[]` | +| `extraVolumeMounts` | Additional grafana server volume mounts | `[]` | +| `createConfigmap` | Enable creating the grafana configmap | `true` | +| `extraConfigmapMounts` | Additional grafana server configMap volume mounts (values are templated) | `[]` | +| `extraEmptyDirMounts` | Additional grafana server emptyDir volume mounts | `[]` | +| `plugins` | Plugins to be loaded along with Grafana | `[]` | +| `datasources` | Configure grafana datasources (passed through tpl) | `{}` | +| `alerting` | Configure grafana alerting (passed through tpl) | `{}` | +| `notifiers` | Configure grafana notifiers | `{}` | +| `dashboardProviders` | Configure grafana dashboard providers | `{}` | +| `dashboards` | Dashboards to import | `{}` | +| `dashboardsConfigMaps` | ConfigMaps reference that contains dashboards | `{}` | +| `grafana.ini` | Grafana's primary configuration | `{}` | +| `global.imagePullSecrets` | Global image pull secrets (can be templated). Allows either an array of {name: pullSecret} maps (k8s-style), or an array of strings (more common helm-style). | `[]` | +| `ldap.enabled` | Enable LDAP authentication | `false` | +| `ldap.existingSecret` | The name of an existing secret containing the `ldap.toml` file, this must have the key `ldap-toml`. | `""` | +| `ldap.config` | Grafana's LDAP configuration | `""` | +| `annotations` | Deployment annotations | `{}` | +| `labels` | Deployment labels | `{}` | +| `podAnnotations` | Pod annotations | `{}` | +| `podLabels` | Pod labels | `{}` | +| `podPortName` | Name of the grafana port on the pod | `grafana` | +| `lifecycleHooks` | Lifecycle hooks for podStart and preStop [Example](https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/#define-poststart-and-prestop-handlers) | `{}` | +| `sidecar.image.repository` | Sidecar image repository | `quay.io/kiwigrid/k8s-sidecar` | +| `sidecar.image.tag` | Sidecar image tag | `1.24.3` | +| `sidecar.image.sha` | Sidecar image sha (optional) | `""` | +| `sidecar.imagePullPolicy` | Sidecar image pull policy | `IfNotPresent` | +| `sidecar.resources` | Sidecar resources | `{}` | +| `sidecar.securityContext` | Sidecar securityContext | `{}` | +| `sidecar.enableUniqueFilenames` | Sets the kiwigrid/k8s-sidecar UNIQUE_FILENAMES environment variable. If set to `true` the sidecar will create unique filenames where duplicate data keys exist between ConfigMaps and/or Secrets within the same or multiple Namespaces. | `false` | +| `sidecar.alerts.enabled` | Enables the cluster wide search for alerts and adds/updates/deletes them in grafana |`false` | +| `sidecar.alerts.label` | Label that config maps with alerts should have to be added | `grafana_alert` | +| `sidecar.alerts.labelValue` | Label value that config maps with alerts should have to be added | `""` | +| `sidecar.alerts.searchNamespace` | Namespaces list. If specified, the sidecar will search for alerts config-maps inside these namespaces. Otherwise the namespace in which the sidecar is running will be used. It's also possible to specify ALL to search in all namespaces. | `nil` | +| `sidecar.alerts.watchMethod` | Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. | `WATCH` | +| `sidecar.alerts.resource` | Should the sidecar looks into secrets, configmaps or both. | `both` | +| `sidecar.alerts.reloadURL` | Full url of datasource configuration reload API endpoint, to invoke after a config-map change | `"http://localhost:3000/api/admin/provisioning/alerting/reload"` | +| `sidecar.alerts.skipReload` | Enabling this omits defining the REQ_URL and REQ_METHOD environment variables | `false` | +| `sidecar.alerts.initDatasources` | Set to true to deploy the datasource sidecar as an initContainer in addition to a container. This is needed if skipReload is true, to load any alerts defined at startup time. | `false` | +| `sidecar.dashboards.enabled` | Enables the cluster wide search for dashboards and adds/updates/deletes them in grafana | `false` | +| `sidecar.dashboards.SCProvider` | Enables creation of sidecar provider | `true` | +| `sidecar.dashboards.provider.name` | Unique name of the grafana provider | `sidecarProvider` | +| `sidecar.dashboards.provider.orgid` | Id of the organisation, to which the dashboards should be added | `1` | +| `sidecar.dashboards.provider.folder` | Logical folder in which grafana groups dashboards | `""` | +| `sidecar.dashboards.provider.disableDelete` | Activate to avoid the deletion of imported dashboards | `false` | +| `sidecar.dashboards.provider.allowUiUpdates` | Allow updating provisioned dashboards from the UI | `false` | +| `sidecar.dashboards.provider.type` | Provider type | `file` | +| `sidecar.dashboards.provider.foldersFromFilesStructure` | Allow Grafana to replicate dashboard structure from filesystem. | `false` | +| `sidecar.dashboards.watchMethod` | Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. | `WATCH` | +| `sidecar.skipTlsVerify` | Set to true to skip tls verification for kube api calls | `nil` | +| `sidecar.dashboards.label` | Label that config maps with dashboards should have to be added | `grafana_dashboard` | +| `sidecar.dashboards.labelValue` | Label value that config maps with dashboards should have to be added | `""` | +| `sidecar.dashboards.folder` | Folder in the pod that should hold the collected dashboards (unless `sidecar.dashboards.defaultFolderName` is set). This path will be mounted. | `/tmp/dashboards` | +| `sidecar.dashboards.folderAnnotation` | The annotation the sidecar will look for in configmaps to override the destination folder for files | `nil` | +| `sidecar.dashboards.defaultFolderName` | The default folder name, it will create a subfolder under the `sidecar.dashboards.folder` and put dashboards in there instead | `nil` | +| `sidecar.dashboards.searchNamespace` | Namespaces list. If specified, the sidecar will search for dashboards config-maps inside these namespaces. Otherwise the namespace in which the sidecar is running will be used. It's also possible to specify ALL to search in all namespaces. | `nil` | +| `sidecar.dashboards.script` | Absolute path to shell script to execute after a configmap got reloaded. | `nil` | +| `sidecar.dashboards.reloadURL` | Full url of dashboards configuration reload API endpoint, to invoke after a config-map change | `"http://localhost:3000/api/admin/provisioning/dashboards/reload"` | +| `sidecar.dashboards.skipReload` | Enabling this omits defining the REQ_URL and REQ_METHOD environment variables | `false` | +| `sidecar.dashboards.resource` | Should the sidecar looks into secrets, configmaps or both. | `both` | +| `sidecar.dashboards.extraMounts` | Additional dashboard sidecar volume mounts. | `[]` | +| `sidecar.datasources.enabled` | Enables the cluster wide search for datasources and adds/updates/deletes them in grafana |`false` | +| `sidecar.datasources.label` | Label that config maps with datasources should have to be added | `grafana_datasource` | +| `sidecar.datasources.labelValue` | Label value that config maps with datasources should have to be added | `""` | +| `sidecar.datasources.searchNamespace` | Namespaces list. If specified, the sidecar will search for datasources config-maps inside these namespaces. Otherwise the namespace in which the sidecar is running will be used. It's also possible to specify ALL to search in all namespaces. | `nil` | +| `sidecar.datasources.watchMethod` | Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. | `WATCH` | +| `sidecar.datasources.resource` | Should the sidecar looks into secrets, configmaps or both. | `both` | +| `sidecar.datasources.reloadURL` | Full url of datasource configuration reload API endpoint, to invoke after a config-map change | `"http://localhost:3000/api/admin/provisioning/datasources/reload"` | +| `sidecar.datasources.skipReload` | Enabling this omits defining the REQ_URL and REQ_METHOD environment variables | `false` | +| `sidecar.datasources.initDatasources` | Set to true to deploy the datasource sidecar as an initContainer in addition to a container. This is needed if skipReload is true, to load any datasources defined at startup time. | `false` | +| `sidecar.notifiers.enabled` | Enables the cluster wide search for notifiers and adds/updates/deletes them in grafana | `false` | +| `sidecar.notifiers.label` | Label that config maps with notifiers should have to be added | `grafana_notifier` | +| `sidecar.notifiers.labelValue` | Label value that config maps with notifiers should have to be added | `""` | +| `sidecar.notifiers.searchNamespace` | Namespaces list. If specified, the sidecar will search for notifiers config-maps (or secrets) inside these namespaces. Otherwise the namespace in which the sidecar is running will be used. It's also possible to specify ALL to search in all namespaces. | `nil` | +| `sidecar.notifiers.watchMethod` | Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. | `WATCH` | +| `sidecar.notifiers.resource` | Should the sidecar looks into secrets, configmaps or both. | `both` | +| `sidecar.notifiers.reloadURL` | Full url of notifier configuration reload API endpoint, to invoke after a config-map change | `"http://localhost:3000/api/admin/provisioning/notifications/reload"` | +| `sidecar.notifiers.skipReload` | Enabling this omits defining the REQ_URL and REQ_METHOD environment variables | `false` | +| `sidecar.notifiers.initNotifiers` | Set to true to deploy the notifier sidecar as an initContainer in addition to a container. This is needed if skipReload is true, to load any notifiers defined at startup time. | `false` | +| `smtp.existingSecret` | The name of an existing secret containing the SMTP credentials. | `""` | +| `smtp.userKey` | The key in the existing SMTP secret containing the username. | `"user"` | +| `smtp.passwordKey` | The key in the existing SMTP secret containing the password. | `"password"` | +| `admin.existingSecret` | The name of an existing secret containing the admin credentials (can be templated). | `""` | +| `admin.userKey` | The key in the existing admin secret containing the username. | `"admin-user"` | +| `admin.passwordKey` | The key in the existing admin secret containing the password. | `"admin-password"` | +| `serviceAccount.autoMount` | Automount the service account token in the pod| `true` | +| `serviceAccount.annotations` | ServiceAccount annotations | | +| `serviceAccount.create` | Create service account | `true` | +| `serviceAccount.labels` | ServiceAccount labels | `{}` | +| `serviceAccount.name` | Service account name to use, when empty will be set to created account if `serviceAccount.create` is set else to `default` | `` | +| `serviceAccount.nameTest` | Service account name to use for test, when empty will be set to created account if `serviceAccount.create` is set else to `default` | `nil` | +| `rbac.create` | Create and use RBAC resources | `true` | +| `rbac.namespaced` | Creates Role and Rolebinding instead of the default ClusterRole and ClusteRoleBindings for the grafana instance | `false` | +| `rbac.useExistingRole` | Set to a rolename to use existing role - skipping role creating - but still doing serviceaccount and rolebinding to the rolename set here. | `nil` | +| `rbac.pspEnabled` | Create PodSecurityPolicy (with `rbac.create`, grant roles permissions as well) | `false` | +| `rbac.pspUseAppArmor` | Enforce AppArmor in created PodSecurityPolicy (requires `rbac.pspEnabled`) | `false` | +| `rbac.extraRoleRules` | Additional rules to add to the Role | [] | +| `rbac.extraClusterRoleRules` | Additional rules to add to the ClusterRole | [] | +| `command` | Define command to be executed by grafana container at startup | `nil` | +| `args` | Define additional args if command is used | `nil` | +| `testFramework.enabled` | Whether to create test-related resources | `true` | +| `testFramework.image` | `test-framework` image repository. | `bats/bats` | +| `testFramework.tag` | `test-framework` image tag. | `v1.4.1` | +| `testFramework.imagePullPolicy` | `test-framework` image pull policy. | `IfNotPresent` | +| `testFramework.securityContext` | `test-framework` securityContext | `{}` | +| `downloadDashboards.env` | Environment variables to be passed to the `download-dashboards` container | `{}` | +| `downloadDashboards.envFromSecret` | Name of a Kubernetes secret (must be manually created in the same namespace) containing values to be added to the environment. Can be templated | `""` | +| `downloadDashboards.resources` | Resources of `download-dashboards` container | `{}` | +| `downloadDashboardsImage.repository` | Curl docker image repo | `curlimages/curl` | +| `downloadDashboardsImage.tag` | Curl docker image tag | `7.73.0` | +| `downloadDashboardsImage.sha` | Curl docker image sha (optional) | `""` | +| `downloadDashboardsImage.pullPolicy` | Curl docker image pull policy | `IfNotPresent` | +| `namespaceOverride` | Override the deployment namespace | `""` (`Release.Namespace`) | +| `serviceMonitor.enabled` | Use servicemonitor from prometheus operator | `false` | +| `serviceMonitor.namespace` | Namespace this servicemonitor is installed in | | +| `serviceMonitor.interval` | How frequently Prometheus should scrape | `1m` | +| `serviceMonitor.path` | Path to scrape | `/metrics` | +| `serviceMonitor.scheme` | Scheme to use for metrics scraping | `http` | +| `serviceMonitor.tlsConfig` | TLS configuration block for the endpoint | `{}` | +| `serviceMonitor.labels` | Labels for the servicemonitor passed to Prometheus Operator | `{}` | +| `serviceMonitor.scrapeTimeout` | Timeout after which the scrape is ended | `30s` | +| `serviceMonitor.relabelings` | MetricRelabelConfigs to apply to samples before ingestion. | `[]` | +| `revisionHistoryLimit` | Number of old ReplicaSets to retain | `10` | +| `imageRenderer.enabled` | Enable the image-renderer deployment & service | `false` | +| `imageRenderer.image.repository` | image-renderer Image repository | `grafana/grafana-image-renderer` | +| `imageRenderer.image.tag` | image-renderer Image tag | `latest` | +| `imageRenderer.image.sha` | image-renderer Image sha (optional) | `""` | +| `imageRenderer.image.pullPolicy` | image-renderer ImagePullPolicy | `Always` | +| `imageRenderer.env` | extra env-vars for image-renderer | `{}` | +| `imageRenderer.envValueFrom` | Environment variables for image-renderer from alternate sources. See the API docs on [EnvVarSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#envvarsource-v1-core) for format details. Can be templated | `{}` | +| `imageRenderer.serviceAccountName` | image-renderer deployment serviceAccountName | `""` | +| `imageRenderer.securityContext` | image-renderer deployment securityContext | `{}` | +| `imageRenderer.hostAliases` | image-renderer deployment Host Aliases | `[]` | +| `imageRenderer.priorityClassName` | image-renderer deployment priority class | `''` | +| `imageRenderer.service.enabled` | Enable the image-renderer service | `true` | +| `imageRenderer.service.portName` | image-renderer service port name | `http` | +| `imageRenderer.service.port` | image-renderer port used by deployment | `8081` | +| `imageRenderer.service.targetPort` | image-renderer service port used by service | `8081` | +| `imageRenderer.appProtocol` | Adds the appProtocol field to the service | `` | +| `imageRenderer.grafanaSubPath` | Grafana sub path to use for image renderer callback url | `''` | +| `imageRenderer.podPortName` | name of the image-renderer port on the pod | `http` | +| `imageRenderer.revisionHistoryLimit` | number of image-renderer replica sets to keep | `10` | +| `imageRenderer.networkPolicy.limitIngress` | Enable a NetworkPolicy to limit inbound traffic from only the created grafana pods | `true` | +| `imageRenderer.networkPolicy.limitEgress` | Enable a NetworkPolicy to limit outbound traffic to only the created grafana pods | `false` | +| `imageRenderer.resources` | Set resource limits for image-renderer pdos | `{}` | +| `imageRenderer.nodeSelector` | Node labels for pod assignment | `{}` | +| `imageRenderer.tolerations` | Toleration labels for pod assignment | `[]` | +| `imageRenderer.affinity` | Affinity settings for pod assignment | `{}` | +| `networkPolicy.enabled` | Enable creation of NetworkPolicy resources. | `false` | +| `networkPolicy.allowExternal` | Don't require client label for connections | `true` | +| `networkPolicy.explicitNamespacesSelector` | A Kubernetes LabelSelector to explicitly select namespaces from which traffic could be allowed | `{}` | +| `networkPolicy.ingress` | Enable the creation of an ingress network policy | `true` | +| `networkPolicy.egress.enabled` | Enable the creation of an egress network policy | `false` | +| `networkPolicy.egress.ports` | An array of ports to allow for the egress | `[]` | +| `enableKubeBackwardCompatibility` | Enable backward compatibility of kubernetes where pod's defintion version below 1.13 doesn't have the enableServiceLinks option | `false` | + +### Example ingress with path + +With grafana 6.3 and above + +```yaml +grafana.ini: + server: + domain: monitoring.example.com + root_url: "%(protocol)s://%(domain)s/grafana" + serve_from_sub_path: true +ingress: + enabled: true + hosts: + - "monitoring.example.com" + path: "/grafana" +``` + +### Example of extraVolumeMounts + +Volume can be type persistentVolumeClaim or hostPath but not both at same time. +If neither existingClaim or hostPath argument is given then type is emptyDir. + +```yaml +- extraVolumeMounts: + - name: plugins + mountPath: /var/lib/grafana/plugins + subPath: configs/grafana/plugins + existingClaim: existing-grafana-claim + readOnly: false + - name: dashboards + mountPath: /var/lib/grafana/dashboards + hostPath: /usr/shared/grafana/dashboards + readOnly: false +``` + +## Import dashboards + +There are a few methods to import dashboards to Grafana. Below are some examples and explanations as to how to use each method: + +```yaml +dashboards: + default: + some-dashboard: + json: | + { + "annotations": + + ... + # Complete json file here + ... + + "title": "Some Dashboard", + "uid": "abcd1234", + "version": 1 + } + custom-dashboard: + # This is a path to a file inside the dashboards directory inside the chart directory + file: dashboards/custom-dashboard.json + prometheus-stats: + # Ref: https://grafana.com/dashboards/2 + gnetId: 2 + revision: 2 + datasource: Prometheus + loki-dashboard-quick-search: + gnetId: 12019 + revision: 2 + datasource: + - name: DS_PROMETHEUS + value: Prometheus + - name: DS_LOKI + value: Loki + local-dashboard: + url: https://raw.githubusercontent.com/user/repository/master/dashboards/dashboard.json +``` + +## BASE64 dashboards + +Dashboards could be stored on a server that does not return JSON directly and instead of it returns a Base64 encoded file (e.g. Gerrit) +A new parameter has been added to the url use case so if you specify a b64content value equals to true after the url entry a Base64 decoding is applied before save the file to disk. +If this entry is not set or is equals to false not decoding is applied to the file before saving it to disk. + +### Gerrit use case + +Gerrit API for download files has the following schema: where {project-name} and +{file-id} usually has '/' in their values and so they MUST be replaced by %2F so if project-name is user/repo, branch-id is master and file-id is equals to dir1/dir2/dashboard +the url value is + +## Sidecar for dashboards + +If the parameter `sidecar.dashboards.enabled` is set, a sidecar container is deployed in the grafana +pod. This container watches all configmaps (or secrets) in the cluster and filters out the ones with +a label as defined in `sidecar.dashboards.label`. The files defined in those configmaps are written +to a folder and accessed by grafana. Changes to the configmaps are monitored and the imported +dashboards are deleted/updated. + +A recommendation is to use one configmap per dashboard, as a reduction of multiple dashboards inside +one configmap is currently not properly mirrored in grafana. + +Example dashboard config: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: sample-grafana-dashboard + labels: + grafana_dashboard: "1" +data: + k8s-dashboard.json: |- + [...] +``` + +## Sidecar for datasources + +If the parameter `sidecar.datasources.enabled` is set, an init container is deployed in the grafana +pod. This container lists all secrets (or configmaps, though not recommended) in the cluster and +filters out the ones with a label as defined in `sidecar.datasources.label`. The files defined in +those secrets are written to a folder and accessed by grafana on startup. Using these yaml files, +the data sources in grafana can be imported. + +Should you aim for reloading datasources in Grafana each time the config is changed, set `sidecar.datasources.skipReload: false` and adjust `sidecar.datasources.reloadURL` to `http://..svc.cluster.local/api/admin/provisioning/datasources/reload`. + +Secrets are recommended over configmaps for this usecase because datasources usually contain private +data like usernames and passwords. Secrets are the more appropriate cluster resource to manage those. + +Example values to add a postgres datasource as a kubernetes secret: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: grafana-datasources + labels: + grafana_datasource: 'true' # default value for: sidecar.datasources.label +stringData: + pg-db.yaml: |- + apiVersion: 1 + datasources: + - name: My pg db datasource + type: postgres + url: my-postgresql-db:5432 + user: db-readonly-user + secureJsonData: + password: 'SUperSEcretPa$$word' + jsonData: + database: my_datase + sslmode: 'disable' # disable/require/verify-ca/verify-full + maxOpenConns: 0 # Grafana v5.4+ + maxIdleConns: 2 # Grafana v5.4+ + connMaxLifetime: 14400 # Grafana v5.4+ + postgresVersion: 1000 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10 + timescaledb: false + # allow users to edit datasources from the UI. + editable: false +``` + +Example values to add a datasource adapted from [Grafana](http://docs.grafana.org/administration/provisioning/#example-datasource-config-file): + +```yaml +datasources: + datasources.yaml: + apiVersion: 1 + datasources: + # name of the datasource. Required + - name: Graphite + # datasource type. Required + type: graphite + # access mode. proxy or direct (Server or Browser in the UI). Required + access: proxy + # org id. will default to orgId 1 if not specified + orgId: 1 + # url + url: http://localhost:8080 + # database password, if used + password: + # database user, if used + user: + # database name, if used + database: + # enable/disable basic auth + basicAuth: + # basic auth username + basicAuthUser: + # basic auth password + basicAuthPassword: + # enable/disable with credentials headers + withCredentials: + # mark as default datasource. Max one per org + isDefault: + # fields that will be converted to json and stored in json_data + jsonData: + graphiteVersion: "1.1" + tlsAuth: true + tlsAuthWithCACert: true + # json object of data that will be encrypted. + secureJsonData: + tlsCACert: "..." + tlsClientCert: "..." + tlsClientKey: "..." + version: 1 + # allow users to edit datasources from the UI. + editable: false +``` + +## Sidecar for notifiers + +If the parameter `sidecar.notifiers.enabled` is set, an init container is deployed in the grafana +pod. This container lists all secrets (or configmaps, though not recommended) in the cluster and +filters out the ones with a label as defined in `sidecar.notifiers.label`. The files defined in +those secrets are written to a folder and accessed by grafana on startup. Using these yaml files, +the notification channels in grafana can be imported. The secrets must be created before +`helm install` so that the notifiers init container can list the secrets. + +Secrets are recommended over configmaps for this usecase because alert notification channels usually contain +private data like SMTP usernames and passwords. Secrets are the more appropriate cluster resource to manage those. + +Example datasource config adapted from [Grafana](https://grafana.com/docs/grafana/latest/administration/provisioning/#alert-notification-channels): + +```yaml +notifiers: + - name: notification-channel-1 + type: slack + uid: notifier1 + # either + org_id: 2 + # or + org_name: Main Org. + is_default: true + send_reminder: true + frequency: 1h + disable_resolve_message: false + # See `Supported Settings` section for settings supporter for each + # alert notification type. + settings: + recipient: 'XXX' + token: 'xoxb' + uploadImage: true + url: https://slack.com + +delete_notifiers: + - name: notification-channel-1 + uid: notifier1 + org_id: 2 + - name: notification-channel-2 + # default org_id: 1 +``` + +## Provision alert rules, contact points, notification policies and notification templates + +There are two methods to provision alerting configuration in Grafana. Below are some examples and explanations as to how to use each method: + +```yaml +alerting: + team1-alert-rules.yaml: + file: alerting/team1/rules.yaml + team2-alert-rules.yaml: + file: alerting/team2/rules.yaml + team3-alert-rules.yaml: + file: alerting/team3/rules.yaml + notification-policies.yaml: + file: alerting/shared/notification-policies.yaml + notification-templates.yaml: + file: alerting/shared/notification-templates.yaml + contactpoints.yaml: + apiVersion: 1 + contactPoints: + - orgId: 1 + name: Slack channel + receivers: + - uid: default-receiver + type: slack + settings: + # Webhook URL to be filled in + url: "" + # We need to escape double curly braces for the tpl function. + text: '{{ `{{ template "default.message" . }}` }}' + title: '{{ `{{ template "default.title" . }}` }}' +``` + +There are two possibilities: + +* Inlining the file contents as described in the example `values.yaml` and the official [Grafana documentation](https://grafana.com/docs/grafana/next/alerting/set-up/provision-alerting-resources/file-provisioning/). +* Importing a file using a relative path starting from the chart root directory. + +### Important notes on file provisioning + +* The chart supports importing YAML and JSON files. +* The filename must be unique, otherwise one volume mount will overwrite the other. +* In case of inlining, double curly braces that arise from the Grafana configuration format and are not intended as templates for the chart must be escaped. +* The number of total files under `alerting:` is not limited. Each file will end up as a volume mount in the corresponding provisioning folder of the deployed Grafana instance. +* The file size for each import is limited by what the function `.Files.Get` can handle, which suffices for most cases. + +## How to serve Grafana with a path prefix (/grafana) + +In order to serve Grafana with a prefix (e.g., ), add the following to your values.yaml. + +```yaml +ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/rewrite-target: /$1 + nginx.ingress.kubernetes.io/use-regex: "true" + + path: /grafana/?(.*) + hosts: + - k8s.example.dev + +grafana.ini: + server: + root_url: http://localhost:3000/grafana # this host can be localhost +``` + +## How to securely reference secrets in grafana.ini + +This example uses Grafana [file providers](https://grafana.com/docs/grafana/latest/administration/configuration/#file-provider) for secret values and the `extraSecretMounts` configuration flag (Additional grafana server secret mounts) to mount the secrets. + +In grafana.ini: + +```yaml +grafana.ini: + [auth.generic_oauth] + enabled = true + client_id = $__file{/etc/secrets/auth_generic_oauth/client_id} + client_secret = $__file{/etc/secrets/auth_generic_oauth/client_secret} +``` + +Existing secret, or created along with helm: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: auth-generic-oauth-secret +type: Opaque +stringData: + client_id: + client_secret: +``` + +Include in the `extraSecretMounts` configuration flag: + +```yaml +- extraSecretMounts: + - name: auth-generic-oauth-secret-mount + secretName: auth-generic-oauth-secret + defaultMode: 0440 + mountPath: /etc/secrets/auth_generic_oauth + readOnly: true +``` + +### extraSecretMounts using a Container Storage Interface (CSI) provider + +This example uses a CSI driver e.g. retrieving secrets using [Azure Key Vault Provider](https://github.com/Azure/secrets-store-csi-driver-provider-azure) + +```yaml +- extraSecretMounts: + - name: secrets-store-inline + mountPath: /run/secrets + readOnly: true + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: "my-provider" + nodePublishSecretRef: + name: akv-creds +``` + +## Image Renderer Plug-In + +This chart supports enabling [remote image rendering](https://github.com/grafana/grafana-image-renderer/blob/master/README.md#run-in-docker) + +```yaml +imageRenderer: + enabled: true +``` + +### Image Renderer NetworkPolicy + +By default the image-renderer pods will have a network policy which only allows ingress traffic from the created grafana instance + +### High Availability for unified alerting + +If you want to run Grafana in a high availability cluster you need to enable +the headless service by setting `headlessService: true` in your `values.yaml` +file. + +As next step you have to setup the `grafana.ini` in your `values.yaml` in a way +that it will make use of the headless service to obtain all the IPs of the +cluster. You should replace ``{{ Name }}`` with the name of your helm deployment. + +```yaml +grafana.ini: + ... + unified_alerting: + enabled: true + ha_peers: {{ Name }}-headless:9094 + ha_listen_address: ${POD_IP}:9094 + ha_advertise_address: ${POD_IP}:9094 + + alerting: + enabled: false +``` diff --git a/charts/devlake/charts/grafana/ci/default-values.yaml b/charts/devlake/charts/grafana/ci/default-values.yaml new file mode 100644 index 0000000..fc2ba60 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/default-values.yaml @@ -0,0 +1 @@ +# Leave this file empty to ensure that CI runs builds against the default configuration in values.yaml. diff --git a/charts/devlake/charts/grafana/ci/with-affinity-values.yaml b/charts/devlake/charts/grafana/ci/with-affinity-values.yaml new file mode 100644 index 0000000..f5b9b53 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/with-affinity-values.yaml @@ -0,0 +1,16 @@ +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/instance: grafana-test + app.kubernetes.io/name: grafana + topologyKey: failure-domain.beta.kubernetes.io/zone + weight: 100 + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/instance: grafana-test + app.kubernetes.io/name: grafana + topologyKey: kubernetes.io/hostname diff --git a/charts/devlake/charts/grafana/ci/with-dashboard-json-values.yaml b/charts/devlake/charts/grafana/ci/with-dashboard-json-values.yaml new file mode 100644 index 0000000..e0c4e41 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/with-dashboard-json-values.yaml @@ -0,0 +1,53 @@ +dashboards: + my-provider: + my-awesome-dashboard: + # An empty but valid dashboard + json: | + { + "__inputs": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "6.3.5" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [], + "schemaVersion": 19, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s"] + }, + "timezone": "", + "title": "Dummy Dashboard", + "uid": "IdcYQooWk", + "version": 1 + } + datasource: Prometheus diff --git a/charts/devlake/charts/grafana/ci/with-dashboard-values.yaml b/charts/devlake/charts/grafana/ci/with-dashboard-values.yaml new file mode 100644 index 0000000..7b662c5 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/with-dashboard-values.yaml @@ -0,0 +1,19 @@ +dashboards: + my-provider: + my-awesome-dashboard: + gnetId: 10000 + revision: 1 + datasource: Prometheus +dashboardProviders: + dashboardproviders.yaml: + apiVersion: 1 + providers: + - name: 'my-provider' + orgId: 1 + folder: '' + type: file + updateIntervalSeconds: 10 + disableDeletion: true + editable: true + options: + path: /var/lib/grafana/dashboards/my-provider diff --git a/charts/devlake/charts/grafana/ci/with-extraconfigmapmounts-values.yaml b/charts/devlake/charts/grafana/ci/with-extraconfigmapmounts-values.yaml new file mode 100644 index 0000000..5cc44a0 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/with-extraconfigmapmounts-values.yaml @@ -0,0 +1,7 @@ +extraConfigmapMounts: + - name: '{{ include "grafana.fullname" . }}' + configMap: '{{ include "grafana.fullname" . }}' + mountPath: /var/lib/grafana/dashboards/test-dashboard.json + # This is not a realistic test, but for this we only care about extraConfigmapMounts not being empty and pointing to an existing ConfigMap + subPath: grafana.ini + readOnly: true diff --git a/charts/devlake/charts/grafana/ci/with-image-renderer-values.yaml b/charts/devlake/charts/grafana/ci/with-image-renderer-values.yaml new file mode 100644 index 0000000..32f3074 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/with-image-renderer-values.yaml @@ -0,0 +1,19 @@ +podLabels: + customLableA: Aaaaa +imageRenderer: + enabled: true + env: + RENDERING_ARGS: --disable-gpu,--window-size=1280x758 + RENDERING_MODE: clustered + podLabels: + customLableB: Bbbbb + networkPolicy: + limitIngress: true + limitEgress: true + resources: + limits: + cpu: 1000m + memory: 1000Mi + requests: + cpu: 500m + memory: 50Mi diff --git a/charts/devlake/charts/grafana/ci/with-persistence.yaml b/charts/devlake/charts/grafana/ci/with-persistence.yaml new file mode 100644 index 0000000..b92ca02 --- /dev/null +++ b/charts/devlake/charts/grafana/ci/with-persistence.yaml @@ -0,0 +1,3 @@ +persistence: + type: pvc + enabled: true diff --git a/charts/devlake/charts/grafana/dashboards/custom-dashboard.json b/charts/devlake/charts/grafana/dashboards/custom-dashboard.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/charts/devlake/charts/grafana/dashboards/custom-dashboard.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/charts/devlake/charts/grafana/templates/NOTES.txt b/charts/devlake/charts/grafana/templates/NOTES.txt new file mode 100644 index 0000000..d86419f --- /dev/null +++ b/charts/devlake/charts/grafana/templates/NOTES.txt @@ -0,0 +1,55 @@ +1. Get your '{{ .Values.adminUser }}' user password by running: + + kubectl get secret --namespace {{ include "grafana.namespace" . }} {{ .Values.admin.existingSecret | default (include "grafana.fullname" .) }} -o jsonpath="{.data.{{ .Values.admin.passwordKey | default "admin-password" }}}" | base64 --decode ; echo + + +2. The Grafana server can be accessed via port {{ .Values.service.port }} on the following DNS name from within your cluster: + + {{ include "grafana.fullname" . }}.{{ include "grafana.namespace" . }}.svc.cluster.local +{{ if .Values.ingress.enabled }} + If you bind grafana to 80, please update values in values.yaml and reinstall: + ``` + securityContext: + runAsUser: 0 + runAsGroup: 0 + fsGroup: 0 + + command: + - "setcap" + - "'cap_net_bind_service=+ep'" + - "/usr/sbin/grafana-server &&" + - "sh" + - "/run.sh" + ``` + Details refer to https://grafana.com/docs/installation/configuration/#http-port. + Or grafana would always crash. + + From outside the cluster, the server URL(s) are: + {{- range .Values.ingress.hosts }} + http://{{ . }} + {{- end }} +{{- else }} + Get the Grafana URL to visit by running these commands in the same shell: + {{- if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ include "grafana.namespace" . }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "grafana.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ include "grafana.namespace" . }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + {{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc --namespace {{ include "grafana.namespace" . }} -w {{ include "grafana.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ include "grafana.namespace" . }} {{ include "grafana.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + http://$SERVICE_IP:{{ .Values.service.port -}} + {{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ include "grafana.namespace" . }} -l "app.kubernetes.io/name={{ include "grafana.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + kubectl --namespace {{ include "grafana.namespace" . }} port-forward $POD_NAME 3000 + {{- end }} +{{- end }} + +3. Login with the password from step 1 and the username: {{ .Values.adminUser }} + +{{- if not .Values.persistence.enabled }} +################################################################################# +###### WARNING: Persistence is disabled!!! You will lose your data when ##### +###### the Grafana pod is terminated. ##### +################################################################################# +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/_helpers.tpl b/charts/devlake/charts/grafana/templates/_helpers.tpl new file mode 100644 index 0000000..8307e10 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/_helpers.tpl @@ -0,0 +1,201 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "grafana.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "grafana.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "grafana.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create the name of the service account +*/}} +{{- define "grafana.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "grafana.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "grafana.serviceAccountNameTest" -}} +{{- if .Values.serviceAccount.create }} +{{- default (print (include "grafana.fullname" .) "-test") .Values.serviceAccount.nameTest }} +{{- else }} +{{- default "default" .Values.serviceAccount.nameTest }} +{{- end }} +{{- end }} + +{{/* +Allow the release namespace to be overridden for multi-namespace deployments in combined charts +*/}} +{{- define "grafana.namespace" -}} +{{- if .Values.namespaceOverride }} +{{- .Values.namespaceOverride }} +{{- else }} +{{- .Release.Namespace }} +{{- end }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "grafana.labels" -}} +helm.sh/chart: {{ include "grafana.chart" . }} +{{ include "grafana.selectorLabels" . }} +{{- if or .Chart.AppVersion .Values.image.tag }} +app.kubernetes.io/version: {{ mustRegexReplaceAllLiteral "@sha.*" .Values.image.tag "" | default .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.extraLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "grafana.selectorLabels" -}} +app.kubernetes.io/name: {{ include "grafana.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "grafana.imageRenderer.labels" -}} +helm.sh/chart: {{ include "grafana.chart" . }} +{{ include "grafana.imageRenderer.selectorLabels" . }} +{{- if or .Chart.AppVersion .Values.image.tag }} +app.kubernetes.io/version: {{ mustRegexReplaceAllLiteral "@sha.*" .Values.image.tag "" | default .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels ImageRenderer +*/}} +{{- define "grafana.imageRenderer.selectorLabels" -}} +app.kubernetes.io/name: {{ include "grafana.name" . }}-image-renderer +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Looks if there's an existing secret and reuse its password. If not it generates +new password and use it. +*/}} +{{- define "grafana.password" -}} +{{- $secret := (lookup "v1" "Secret" (include "grafana.namespace" .) (include "grafana.fullname" .) ) }} +{{- if $secret }} +{{- index $secret "data" "admin-password" }} +{{- else }} +{{- (randAlphaNum 40) | b64enc | quote }} +{{- end }} +{{- end }} + +{{/* +Return the appropriate apiVersion for rbac. +*/}} +{{- define "grafana.rbac.apiVersion" -}} +{{- if $.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" }} +{{- print "rbac.authorization.k8s.io/v1" }} +{{- else }} +{{- print "rbac.authorization.k8s.io/v1beta1" }} +{{- end }} +{{- end }} + +{{/* +Return the appropriate apiVersion for ingress. +*/}} +{{- define "grafana.ingress.apiVersion" -}} +{{- if and ($.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" .Capabilities.KubeVersion.Version) }} +{{- print "networking.k8s.io/v1" }} +{{- else if $.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }} +{{- print "networking.k8s.io/v1beta1" }} +{{- else }} +{{- print "extensions/v1beta1" }} +{{- end }} +{{- end }} + +{{/* +Return the appropriate apiVersion for Horizontal Pod Autoscaler. +*/}} +{{- define "grafana.hpa.apiVersion" -}} +{{- if $.Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler" }} +{{- print "autoscaling/v2" }} +{{- else if $.Capabilities.APIVersions.Has "autoscaling/v2beta2/HorizontalPodAutoscaler" }} +{{- print "autoscaling/v2beta2" }} +{{- else }} +{{- print "autoscaling/v2beta1" }} +{{- end }} +{{- end }} + +{{/* +Return the appropriate apiVersion for podDisruptionBudget. +*/}} +{{- define "grafana.podDisruptionBudget.apiVersion" -}} +{{- if $.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} +{{- print "policy/v1" }} +{{- else }} +{{- print "policy/v1beta1" }} +{{- end }} +{{- end }} + +{{/* +Return if ingress is stable. +*/}} +{{- define "grafana.ingress.isStable" -}} +{{- eq (include "grafana.ingress.apiVersion" .) "networking.k8s.io/v1" }} +{{- end }} + +{{/* +Return if ingress supports ingressClassName. +*/}} +{{- define "grafana.ingress.supportsIngressClassName" -}} +{{- or (eq (include "grafana.ingress.isStable" .) "true") (and (eq (include "grafana.ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version)) }} +{{- end }} + +{{/* +Return if ingress supports pathType. +*/}} +{{- define "grafana.ingress.supportsPathType" -}} +{{- or (eq (include "grafana.ingress.isStable" .) "true") (and (eq (include "grafana.ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version)) }} +{{- end }} + +{{/* +Formats imagePullSecrets. Input is (dict "root" . "imagePullSecrets" .{specific imagePullSecrets}) +*/}} +{{- define "grafana.imagePullSecrets" -}} +{{- $root := .root }} +{{- range (concat .root.Values.global.imagePullSecrets .imagePullSecrets) }} +{{- if eq (typeOf .) "map[string]interface {}" }} +- {{ toYaml (dict "name" (tpl .name $root)) | trim }} +{{- else }} +- name: {{ tpl . $root }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/_pod.tpl b/charts/devlake/charts/grafana/templates/_pod.tpl new file mode 100644 index 0000000..762603d --- /dev/null +++ b/charts/devlake/charts/grafana/templates/_pod.tpl @@ -0,0 +1,1159 @@ +{{- define "grafana.pod" -}} +{{- $sts := list "sts" "StatefulSet" "statefulset" -}} +{{- $root := . -}} +{{- with .Values.schedulerName }} +schedulerName: "{{ . }}" +{{- end }} +serviceAccountName: {{ include "grafana.serviceAccountName" . }} +automountServiceAccountToken: {{ .Values.serviceAccount.autoMount }} +{{- with .Values.securityContext }} +securityContext: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- with .Values.hostAliases }} +hostAliases: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- with .Values.priorityClassName }} +priorityClassName: {{ . }} +{{- end }} +{{- if ( or .Values.persistence.enabled .Values.dashboards .Values.extraInitContainers (and .Values.sidecar.datasources.enabled .Values.sidecar.datasources.initDatasources) (and .Values.sidecar.notifiers.enabled .Values.sidecar.notifiers.initNotifiers)) }} +initContainers: +{{- end }} +{{- if ( and .Values.persistence.enabled .Values.initChownData.enabled ) }} + - name: init-chown-data + {{- if .Values.initChownData.image.sha }} + image: "{{ .Values.initChownData.image.repository }}:{{ .Values.initChownData.image.tag }}@sha256:{{ .Values.initChownData.image.sha }}" + {{- else }} + image: "{{ .Values.initChownData.image.repository }}:{{ .Values.initChownData.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.initChownData.image.pullPolicy }} + {{- with .Values.initChownData.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + command: + - chown + - -R + - {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.runAsGroup }} + - /var/lib/grafana + {{- with .Values.initChownData.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: storage + mountPath: "/var/lib/grafana" + {{- with .Values.persistence.subPath }} + subPath: {{ tpl . $root }} + {{- end }} +{{- end }} +{{- if .Values.dashboards }} + - name: download-dashboards + {{- if .Values.downloadDashboardsImage.sha }} + image: "{{ .Values.downloadDashboardsImage.repository }}:{{ .Values.downloadDashboardsImage.tag }}@sha256:{{ .Values.downloadDashboardsImage.sha }}" + {{- else }} + image: "{{ .Values.downloadDashboardsImage.repository }}:{{ .Values.downloadDashboardsImage.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.downloadDashboardsImage.pullPolicy }} + command: ["/bin/sh"] + args: [ "-c", "mkdir -p /var/lib/grafana/dashboards/default && /bin/sh -x /etc/grafana/download_dashboards.sh" ] + {{- with .Values.downloadDashboards.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + env: + {{- range $key, $value := .Values.downloadDashboards.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- range $key, $value := .Values.downloadDashboards.envValueFrom }} + - name: {{ $key | quote }} + valueFrom: + {{- tpl (toYaml $value) $ | nindent 10 }} + {{- end }} + {{- with .Values.downloadDashboards.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.downloadDashboards.envFromSecret }} + envFrom: + - secretRef: + name: {{ tpl . $root }} + {{- end }} + volumeMounts: + - name: config + mountPath: "/etc/grafana/download_dashboards.sh" + subPath: download_dashboards.sh + - name: storage + mountPath: "/var/lib/grafana" + {{- with .Values.persistence.subPath }} + subPath: {{ tpl . $root }} + {{- end }} + {{- range .Values.extraSecretMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + readOnly: {{ .readOnly }} + {{- end }} +{{- end }} +{{- if and .Values.sidecar.datasources.enabled .Values.sidecar.datasources.initDatasources }} + - name: {{ include "grafana.name" . }}-init-sc-datasources + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.datasources.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.datasources.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: "LIST" + - name: LABEL + value: "{{ .Values.sidecar.datasources.label }}" + {{- with .Values.sidecar.datasources.labelValue }} + - name: LABEL_VALUE + value: {{ quote . }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.datasources.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.datasources.logLevel }} + {{- end }} + - name: FOLDER + value: "/etc/grafana/provisioning/datasources" + - name: RESOURCE + value: {{ quote .Values.sidecar.datasources.resource }} + {{- with .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ . }}" + {{- end }} + {{- if .Values.sidecar.datasources.searchNamespace }} + - name: NAMESPACE + value: "{{ tpl (.Values.sidecar.datasources.searchNamespace | join ",") . }}" + {{- end }} + {{- with .Values.sidecar.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-datasources-volume + mountPath: "/etc/grafana/provisioning/datasources" +{{- end }} +{{- if and .Values.sidecar.notifiers.enabled .Values.sidecar.notifiers.initNotifiers }} + - name: {{ include "grafana.name" . }}-init-sc-notifiers + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.notifiers.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.notifiers.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: LIST + - name: LABEL + value: "{{ .Values.sidecar.notifiers.label }}" + {{- with .Values.sidecar.notifiers.labelValue }} + - name: LABEL_VALUE + value: {{ quote . }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.notifiers.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.notifiers.logLevel }} + {{- end }} + - name: FOLDER + value: "/etc/grafana/provisioning/notifiers" + - name: RESOURCE + value: {{ quote .Values.sidecar.notifiers.resource }} + {{- with .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.notifiers.searchNamespace }} + - name: NAMESPACE + value: "{{ tpl (. | join ",") $root }}" + {{- end }} + {{- with .Values.sidecar.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-notifiers-volume + mountPath: "/etc/grafana/provisioning/notifiers" +{{- end}} +{{- with .Values.extraInitContainers }} + {{- tpl (toYaml .) $root | nindent 2 }} +{{- end }} +{{- if or .Values.image.pullSecrets .Values.global.imagePullSecrets }} +imagePullSecrets: + {{- include "grafana.imagePullSecrets" (dict "root" $root "imagePullSecrets" .Values.image.pullSecrets) | nindent 2 }} +{{- end }} +{{- if not .Values.enableKubeBackwardCompatibility }} +enableServiceLinks: {{ .Values.enableServiceLinks }} +{{- end }} +containers: +{{- if .Values.sidecar.alerts.enabled }} + - name: {{ include "grafana.name" . }}-sc-alerts + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.alerts.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.alerts.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: {{ .Values.sidecar.alerts.watchMethod }} + - name: LABEL + value: "{{ .Values.sidecar.alerts.label }}" + {{- with .Values.sidecar.alerts.labelValue }} + - name: LABEL_VALUE + value: {{ quote . }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.alerts.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.alerts.logLevel }} + {{- end }} + - name: FOLDER + value: "/etc/grafana/provisioning/alerting" + - name: RESOURCE + value: {{ quote .Values.sidecar.alerts.resource }} + {{- with .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.alerts.searchNamespace }} + - name: NAMESPACE + value: {{ . | join "," | quote }} + {{- end }} + {{- with .Values.sidecar.alerts.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: {{ quote . }} + {{- end }} + {{- with .Values.sidecar.alerts.script }} + - name: SCRIPT + value: {{ quote . }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_USER) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_USERNAME + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.userKey | default "admin-user" }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_PASSWORD + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.passwordKey | default "admin-password" }} + {{- end }} + {{- if not .Values.sidecar.alerts.skipReload }} + - name: REQ_URL + value: {{ .Values.sidecar.alerts.reloadURL }} + - name: REQ_METHOD + value: POST + {{- end }} + {{- if .Values.sidecar.alerts.watchServerTimeout }} + {{- if ne .Values.sidecar.alerts.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.alerts.watchServerTimeout with .Values.sidecar.alerts.watchMethod %s" .Values.sidecar.alerts.watchMethod) }} + {{- end }} + - name: WATCH_SERVER_TIMEOUT + value: "{{ .Values.sidecar.alerts.watchServerTimeout }}" + {{- end }} + {{- if .Values.sidecar.alerts.watchClientTimeout }} + {{- if ne .Values.sidecar.alerts.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.alerts.watchClientTimeout with .Values.sidecar.alerts.watchMethod %s" .Values.sidecar.alerts.watchMethod) }} + {{- end }} + - name: WATCH_CLIENT_TIMEOUT + value: "{{ .Values.sidecar.alerts.watchClientTimeout }}" + {{- end }} + {{- with .Values.sidecar.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-alerts-volume + mountPath: "/etc/grafana/provisioning/alerting" +{{- end}} +{{- if .Values.sidecar.dashboards.enabled }} + - name: {{ include "grafana.name" . }}-sc-dashboard + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.dashboards.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.dashboards.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: {{ .Values.sidecar.dashboards.watchMethod }} + - name: LABEL + value: "{{ .Values.sidecar.dashboards.label }}" + {{- with .Values.sidecar.dashboards.labelValue }} + - name: LABEL_VALUE + value: {{ quote . }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.dashboards.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.dashboards.logLevel }} + {{- end }} + - name: FOLDER + value: "{{ .Values.sidecar.dashboards.folder }}{{- with .Values.sidecar.dashboards.defaultFolderName }}/{{ . }}{{- end }}" + - name: RESOURCE + value: {{ quote .Values.sidecar.dashboards.resource }} + {{- with .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.dashboards.searchNamespace }} + - name: NAMESPACE + value: "{{ tpl (. | join ",") $root }}" + {{- end }} + {{- with .Values.sidecar.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.dashboards.folderAnnotation }} + - name: FOLDER_ANNOTATION + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.dashboards.script }} + - name: SCRIPT + value: "{{ . }}" + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_USER) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_USERNAME + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.userKey | default "admin-user" }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_PASSWORD + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.passwordKey | default "admin-password" }} + {{- end }} + {{- if not .Values.sidecar.dashboards.skipReload }} + - name: REQ_URL + value: {{ .Values.sidecar.dashboards.reloadURL }} + - name: REQ_METHOD + value: POST + {{- end }} + {{- if .Values.sidecar.dashboards.watchServerTimeout }} + {{- if ne .Values.sidecar.dashboards.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.dashboards.watchServerTimeout with .Values.sidecar.dashboards.watchMethod %s" .Values.sidecar.dashboards.watchMethod) }} + {{- end }} + - name: WATCH_SERVER_TIMEOUT + value: "{{ .Values.sidecar.dashboards.watchServerTimeout }}" + {{- end }} + {{- if .Values.sidecar.dashboards.watchClientTimeout }} + {{- if ne .Values.sidecar.dashboards.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.dashboards.watchClientTimeout with .Values.sidecar.dashboards.watchMethod %s" .Values.sidecar.dashboards.watchMethod) }} + {{- end }} + - name: WATCH_CLIENT_TIMEOUT + value: {{ .Values.sidecar.dashboards.watchClientTimeout | quote }} + {{- end }} + {{- with .Values.sidecar.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-dashboard-volume + mountPath: {{ .Values.sidecar.dashboards.folder | quote }} + {{- with .Values.sidecar.dashboards.extraMounts }} + {{- toYaml . | trim | nindent 6 }} + {{- end }} +{{- end}} +{{- if .Values.sidecar.datasources.enabled }} + - name: {{ include "grafana.name" . }}-sc-datasources + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.datasources.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.datasources.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: {{ .Values.sidecar.datasources.watchMethod }} + - name: LABEL + value: "{{ .Values.sidecar.datasources.label }}" + {{- with .Values.sidecar.datasources.labelValue }} + - name: LABEL_VALUE + value: {{ quote . }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.datasources.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.datasources.logLevel }} + {{- end }} + - name: FOLDER + value: "/etc/grafana/provisioning/datasources" + - name: RESOURCE + value: {{ quote .Values.sidecar.datasources.resource }} + {{- with .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.datasources.searchNamespace }} + - name: NAMESPACE + value: "{{ tpl (. | join ",") $root }}" + {{- end }} + {{- if .Values.sidecar.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: "{{ .Values.sidecar.skipTlsVerify }}" + {{- end }} + {{- if .Values.sidecar.datasources.script }} + - name: SCRIPT + value: "{{ .Values.sidecar.datasources.script }}" + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_USER) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_USERNAME + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.userKey | default "admin-user" }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_PASSWORD + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.passwordKey | default "admin-password" }} + {{- end }} + {{- if not .Values.sidecar.datasources.skipReload }} + - name: REQ_URL + value: {{ .Values.sidecar.datasources.reloadURL }} + - name: REQ_METHOD + value: POST + {{- end }} + {{- if .Values.sidecar.datasources.watchServerTimeout }} + {{- if ne .Values.sidecar.datasources.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.datasources.watchServerTimeout with .Values.sidecar.datasources.watchMethod %s" .Values.sidecar.datasources.watchMethod) }} + {{- end }} + - name: WATCH_SERVER_TIMEOUT + value: "{{ .Values.sidecar.datasources.watchServerTimeout }}" + {{- end }} + {{- if .Values.sidecar.datasources.watchClientTimeout }} + {{- if ne .Values.sidecar.datasources.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.datasources.watchClientTimeout with .Values.sidecar.datasources.watchMethod %s" .Values.sidecar.datasources.watchMethod) }} + {{- end }} + - name: WATCH_CLIENT_TIMEOUT + value: "{{ .Values.sidecar.datasources.watchClientTimeout }}" + {{- end }} + {{- with .Values.sidecar.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-datasources-volume + mountPath: "/etc/grafana/provisioning/datasources" +{{- end}} +{{- if .Values.sidecar.notifiers.enabled }} + - name: {{ include "grafana.name" . }}-sc-notifiers + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.notifiers.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.notifiers.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: {{ .Values.sidecar.notifiers.watchMethod }} + - name: LABEL + value: "{{ .Values.sidecar.notifiers.label }}" + {{- with .Values.sidecar.notifiers.labelValue }} + - name: LABEL_VALUE + value: {{ quote . }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.notifiers.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.notifiers.logLevel }} + {{- end }} + - name: FOLDER + value: "/etc/grafana/provisioning/notifiers" + - name: RESOURCE + value: {{ quote .Values.sidecar.notifiers.resource }} + {{- if .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ .Values.sidecar.enableUniqueFilenames }}" + {{- end }} + {{- with .Values.sidecar.notifiers.searchNamespace }} + - name: NAMESPACE + value: "{{ tpl (. | join ",") $root }}" + {{- end }} + {{- with .Values.sidecar.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: "{{ . }}" + {{- end }} + {{- if .Values.sidecar.notifiers.script }} + - name: SCRIPT + value: "{{ .Values.sidecar.notifiers.script }}" + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_USER) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_USERNAME + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.userKey | default "admin-user" }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_PASSWORD + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.passwordKey | default "admin-password" }} + {{- end }} + {{- if not .Values.sidecar.notifiers.skipReload }} + - name: REQ_URL + value: {{ .Values.sidecar.notifiers.reloadURL }} + - name: REQ_METHOD + value: POST + {{- end }} + {{- if .Values.sidecar.notifiers.watchServerTimeout }} + {{- if ne .Values.sidecar.notifiers.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.notifiers.watchServerTimeout with .Values.sidecar.notifiers.watchMethod %s" .Values.sidecar.notifiers.watchMethod) }} + {{- end }} + - name: WATCH_SERVER_TIMEOUT + value: "{{ .Values.sidecar.notifiers.watchServerTimeout }}" + {{- end }} + {{- if .Values.sidecar.notifiers.watchClientTimeout }} + {{- if ne .Values.sidecar.notifiers.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.notifiers.watchClientTimeout with .Values.sidecar.notifiers.watchMethod %s" .Values.sidecar.notifiers.watchMethod) }} + {{- end }} + - name: WATCH_CLIENT_TIMEOUT + value: "{{ .Values.sidecar.notifiers.watchClientTimeout }}" + {{- end }} + {{- with .Values.sidecar.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-notifiers-volume + mountPath: "/etc/grafana/provisioning/notifiers" +{{- end}} +{{- if .Values.sidecar.plugins.enabled }} + - name: {{ include "grafana.name" . }}-sc-plugins + {{- if .Values.sidecar.image.sha }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}@sha256:{{ .Values.sidecar.image.sha }}" + {{- else }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.sidecar.imagePullPolicy }} + env: + {{- range $key, $value := .Values.sidecar.plugins.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + {{- if .Values.sidecar.plugins.ignoreAlreadyProcessed }} + - name: IGNORE_ALREADY_PROCESSED + value: "true" + {{- end }} + - name: METHOD + value: {{ .Values.sidecar.plugins.watchMethod }} + - name: LABEL + value: "{{ .Values.sidecar.plugins.label }}" + {{- if .Values.sidecar.plugins.labelValue }} + - name: LABEL_VALUE + value: {{ quote .Values.sidecar.plugins.labelValue }} + {{- end }} + {{- if or .Values.sidecar.logLevel .Values.sidecar.plugins.logLevel }} + - name: LOG_LEVEL + value: {{ default .Values.sidecar.logLevel .Values.sidecar.plugins.logLevel }} + {{- end }} + - name: FOLDER + value: "/etc/grafana/provisioning/plugins" + - name: RESOURCE + value: {{ quote .Values.sidecar.plugins.resource }} + {{- with .Values.sidecar.enableUniqueFilenames }} + - name: UNIQUE_FILENAMES + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.plugins.searchNamespace }} + - name: NAMESPACE + value: "{{ tpl (. | join ",") $root }}" + {{- end }} + {{- with .Values.sidecar.plugins.script }} + - name: SCRIPT + value: "{{ . }}" + {{- end }} + {{- with .Values.sidecar.skipTlsVerify }} + - name: SKIP_TLS_VERIFY + value: "{{ . }}" + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_USER) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_USERNAME + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.userKey | default "admin-user" }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: REQ_PASSWORD + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.passwordKey | default "admin-password" }} + {{- end }} + {{- if not .Values.sidecar.plugins.skipReload }} + - name: REQ_URL + value: {{ .Values.sidecar.plugins.reloadURL }} + - name: REQ_METHOD + value: POST + {{- end }} + {{- if .Values.sidecar.plugins.watchServerTimeout }} + {{- if ne .Values.sidecar.plugins.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.plugins.watchServerTimeout with .Values.sidecar.plugins.watchMethod %s" .Values.sidecar.plugins.watchMethod) }} + {{- end }} + - name: WATCH_SERVER_TIMEOUT + value: "{{ .Values.sidecar.plugins.watchServerTimeout }}" + {{- end }} + {{- if .Values.sidecar.plugins.watchClientTimeout }} + {{- if ne .Values.sidecar.plugins.watchMethod "WATCH" }} + {{- fail (printf "Cannot use .Values.sidecar.plugins.watchClientTimeout with .Values.sidecar.plugins.watchMethod %s" .Values.sidecar.plugins.watchMethod) }} + {{- end }} + - name: WATCH_CLIENT_TIMEOUT + value: "{{ .Values.sidecar.plugins.watchClientTimeout }}" + {{- end }} + {{- with .Values.sidecar.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.sidecar.securityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: sc-plugins-volume + mountPath: "/etc/grafana/provisioning/plugins" +{{- end}} + - name: {{ .Chart.Name }} + {{- if .Values.image.sha }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}" + {{- else }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.command }} + command: + {{- range .Values.command }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- if .Values.args }} + args: + {{- range .Values.args }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 6 }} + {{- end }} + volumeMounts: + - name: config + mountPath: "/etc/grafana/grafana.ini" + subPath: grafana.ini + {{- if .Values.ldap.enabled }} + - name: ldap + mountPath: "/etc/grafana/ldap.toml" + subPath: ldap.toml + {{- end }} + {{- range .Values.extraConfigmapMounts }} + - name: {{ tpl .name $root }} + mountPath: {{ tpl .mountPath $root }} + subPath: {{ tpl (.subPath | default "") $root }} + readOnly: {{ .readOnly }} + {{- end }} + - name: storage + mountPath: "/var/lib/grafana" + {{- with .Values.persistence.subPath }} + subPath: {{ tpl . $root }} + {{- end }} + {{- with .Values.dashboards }} + {{- range $provider, $dashboards := . }} + {{- range $key, $value := $dashboards }} + {{- if (or (hasKey $value "json") (hasKey $value "file")) }} + - name: dashboards-{{ $provider }} + mountPath: "/var/lib/grafana/dashboards/{{ $provider }}/{{ $key }}.json" + subPath: "{{ $key }}.json" + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- with .Values.dashboardsConfigMaps }} + {{- range (keys . | sortAlpha) }} + - name: dashboards-{{ . }} + mountPath: "/var/lib/grafana/dashboards/{{ . }}" + {{- end }} + {{- end }} + {{- with .Values.datasources }} + {{- range (keys . | sortAlpha) }} + - name: config + mountPath: "/etc/grafana/provisioning/datasources/{{ . }}" + subPath: {{ . | quote }} + {{- end }} + {{- end }} + {{- with .Values.notifiers }} + {{- range (keys . | sortAlpha) }} + - name: config + mountPath: "/etc/grafana/provisioning/notifiers/{{ . }}" + subPath: {{ . | quote }} + {{- end }} + {{- end }} + {{- with .Values.alerting }} + {{- range (keys . | sortAlpha) }} + - name: config + mountPath: "/etc/grafana/provisioning/alerting/{{ . }}" + subPath: {{ . | quote }} + {{- end }} + {{- end }} + {{- with .Values.dashboardProviders }} + {{- range (keys . | sortAlpha) }} + - name: config + mountPath: "/etc/grafana/provisioning/dashboards/{{ . }}" + subPath: {{ . | quote }} + {{- end }} + {{- end }} + {{- with .Values.sidecar.alerts.enabled }} + - name: sc-alerts-volume + mountPath: "/etc/grafana/provisioning/alerting" + {{- end}} + {{- if .Values.sidecar.dashboards.enabled }} + - name: sc-dashboard-volume + mountPath: {{ .Values.sidecar.dashboards.folder | quote }} + {{- if .Values.sidecar.dashboards.SCProvider }} + - name: sc-dashboard-provider + mountPath: "/etc/grafana/provisioning/dashboards/sc-dashboardproviders.yaml" + subPath: provider.yaml + {{- end}} + {{- end}} + {{- if .Values.sidecar.datasources.enabled }} + - name: sc-datasources-volume + mountPath: "/etc/grafana/provisioning/datasources" + {{- end}} + {{- if .Values.sidecar.plugins.enabled }} + - name: sc-plugins-volume + mountPath: "/etc/grafana/provisioning/plugins" + {{- end}} + {{- if .Values.sidecar.notifiers.enabled }} + - name: sc-notifiers-volume + mountPath: "/etc/grafana/provisioning/notifiers" + {{- end}} + {{- range .Values.extraSecretMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + readOnly: {{ .readOnly }} + subPath: {{ .subPath | default "" }} + {{- end }} + {{- range .Values.extraVolumeMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} + {{- end }} + {{- range .Values.extraEmptyDirMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + {{- end }} + ports: + - name: {{ .Values.podPortName }} + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + - name: {{ .Values.gossipPortName }}-tcp + containerPort: 9094 + protocol: TCP + - name: {{ .Values.gossipPortName }}-udp + containerPort: 9094 + protocol: UDP + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- if and (not .Values.env.GF_SECURITY_ADMIN_USER) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: GF_SECURITY_ADMIN_USER + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.userKey | default "admin-user" }} + {{- end }} + {{- if and (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + - name: GF_SECURITY_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ (tpl .Values.admin.existingSecret .) | default (include "grafana.fullname" .) }} + key: {{ .Values.admin.passwordKey | default "admin-password" }} + {{- end }} + {{- if .Values.plugins }} + - name: GF_INSTALL_PLUGINS + valueFrom: + configMapKeyRef: + name: {{ include "grafana.fullname" . }} + key: plugins + {{- end }} + {{- if .Values.smtp.existingSecret }} + - name: GF_SMTP_USER + valueFrom: + secretKeyRef: + name: {{ .Values.smtp.existingSecret }} + key: {{ .Values.smtp.userKey | default "user" }} + - name: GF_SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.smtp.existingSecret }} + key: {{ .Values.smtp.passwordKey | default "password" }} + {{- end }} + {{- if .Values.imageRenderer.enabled }} + - name: GF_RENDERING_SERVER_URL + value: http://{{ include "grafana.fullname" . }}-image-renderer.{{ include "grafana.namespace" . }}:{{ .Values.imageRenderer.service.port }}/render + - name: GF_RENDERING_CALLBACK_URL + value: {{ .Values.imageRenderer.grafanaProtocol }}://{{ include "grafana.fullname" . }}.{{ include "grafana.namespace" . }}:{{ .Values.service.port }}/{{ .Values.imageRenderer.grafanaSubPath }} + {{- end }} + - name: GF_PATHS_DATA + value: {{ (get .Values "grafana.ini").paths.data }} + - name: GF_PATHS_LOGS + value: {{ (get .Values "grafana.ini").paths.logs }} + - name: GF_PATHS_PLUGINS + value: {{ (get .Values "grafana.ini").paths.plugins }} + - name: GF_PATHS_PROVISIONING + value: {{ (get .Values "grafana.ini").paths.provisioning }} + {{- range $key, $value := .Values.envValueFrom }} + - name: {{ $key | quote }} + valueFrom: + {{- tpl (toYaml $value) $ | nindent 10 }} + {{- end }} + {{- range $key, $value := .Values.env }} + - name: "{{ tpl $key $ }}" + value: "{{ tpl (print $value) $ }}" + {{- end }} + {{- if or .Values.envFromSecret (or .Values.envRenderSecret .Values.envFromSecrets) .Values.envFromConfigMaps }} + envFrom: + {{- if .Values.envFromSecret }} + - secretRef: + name: {{ tpl .Values.envFromSecret . }} + {{- end }} + {{- if .Values.envRenderSecret }} + - secretRef: + name: {{ include "grafana.fullname" . }}-env + {{- end }} + {{- range .Values.envFromSecrets }} + - secretRef: + name: {{ tpl .name $ }} + optional: {{ .optional | default false }} + {{- end }} + {{- range .Values.envFromConfigMaps }} + - configMapRef: + name: {{ tpl .name $ }} + optional: {{ .optional | default false }} + {{- end }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.lifecycleHooks }} + lifecycle: + {{- tpl (toYaml .) $root | nindent 6 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 6 }} + {{- end }} +{{- with .Values.extraContainers }} + {{- tpl . $ | nindent 2 }} +{{- end }} +{{- with .Values.nodeSelector }} +nodeSelector: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- with .Values.affinity }} +affinity: + {{- tpl (toYaml .) $root | nindent 2 }} +{{- end }} +{{- with .Values.topologySpreadConstraints }} +topologySpreadConstraints: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- with .Values.tolerations }} +tolerations: + {{- toYaml . | nindent 2 }} +{{- end }} +volumes: + - name: config + configMap: + name: {{ include "grafana.fullname" . }} + {{- range .Values.extraConfigmapMounts }} + - name: {{ tpl .name $root }} + configMap: + name: {{ tpl .configMap $root }} + {{- with .items }} + items: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- if .Values.dashboards }} + {{- range (keys .Values.dashboards | sortAlpha) }} + - name: dashboards-{{ . }} + configMap: + name: {{ include "grafana.fullname" $ }}-dashboards-{{ . }} + {{- end }} + {{- end }} + {{- if .Values.dashboardsConfigMaps }} + {{- range $provider, $name := .Values.dashboardsConfigMaps }} + - name: dashboards-{{ $provider }} + configMap: + name: {{ tpl $name $root }} + {{- end }} + {{- end }} + {{- if .Values.ldap.enabled }} + - name: ldap + secret: + {{- if .Values.ldap.existingSecret }} + secretName: {{ .Values.ldap.existingSecret }} + {{- else }} + secretName: {{ include "grafana.fullname" . }} + {{- end }} + items: + - key: ldap-toml + path: ldap.toml + {{- end }} + {{- if and .Values.persistence.enabled (eq .Values.persistence.type "pvc") }} + - name: storage + persistentVolumeClaim: + claimName: {{ tpl (.Values.persistence.existingClaim | default (include "grafana.fullname" .)) . }} + {{- else if and .Values.persistence.enabled (has .Values.persistence.type $sts) }} + {{/* nothing */}} + {{- else }} + - name: storage + {{- if .Values.persistence.inMemory.enabled }} + emptyDir: + medium: Memory + {{- with .Values.persistence.inMemory.sizeLimit }} + sizeLimit: {{ . }} + {{- end }} + {{- else }} + emptyDir: {} + {{- end }} + {{- end }} + {{- if .Values.sidecar.alerts.enabled }} + - name: sc-alerts-volume + emptyDir: + {{- with .Values.sidecar.alerts.sizeLimit }} + sizeLimit: {{ . }} + {{- else }} + {} + {{- end }} + {{- end }} + {{- if .Values.sidecar.dashboards.enabled }} + - name: sc-dashboard-volume + emptyDir: + {{- with .Values.sidecar.dashboards.sizeLimit }} + sizeLimit: {{ . }} + {{- else }} + {} + {{- end }} + {{- if .Values.sidecar.dashboards.SCProvider }} + - name: sc-dashboard-provider + configMap: + name: {{ include "grafana.fullname" . }}-config-dashboards + {{- end }} + {{- end }} + {{- if .Values.sidecar.datasources.enabled }} + - name: sc-datasources-volume + emptyDir: + {{- with .Values.sidecar.datasources.sizeLimit }} + sizeLimit: {{ . }} + {{- else }} + {} + {{- end }} + {{- end }} + {{- if .Values.sidecar.plugins.enabled }} + - name: sc-plugins-volume + emptyDir: + {{- with .Values.sidecar.plugins.sizeLimit }} + sizeLimit: {{ . }} + {{- else }} + {} + {{- end }} + {{- end }} + {{- if .Values.sidecar.notifiers.enabled }} + - name: sc-notifiers-volume + emptyDir: + {{- with .Values.sidecar.notifiers.sizeLimit }} + sizeLimit: {{ . }} + {{- else }} + {} + {{- end }} + {{- end }} + {{- range .Values.extraSecretMounts }} + {{- if .secretName }} + - name: {{ .name }} + secret: + secretName: {{ .secretName }} + defaultMode: {{ .defaultMode }} + {{- with .items }} + items: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- else if .projected }} + - name: {{ .name }} + projected: + {{- toYaml .projected | nindent 6 }} + {{- else if .csi }} + - name: {{ .name }} + csi: + {{- toYaml .csi | nindent 6 }} + {{- end }} + {{- end }} + {{- range .Values.extraVolumeMounts }} + - name: {{ .name }} + {{- if .existingClaim }} + persistentVolumeClaim: + claimName: {{ .existingClaim }} + {{- else if .hostPath }} + hostPath: + path: {{ .hostPath }} + {{- else if .csi }} + csi: + {{- toYaml .data | nindent 6 }} + {{- else }} + emptyDir: {} + {{- end }} + {{- end }} + {{- range .Values.extraEmptyDirMounts }} + - name: {{ .name }} + emptyDir: {} + {{- end }} + {{- with .Values.extraContainerVolumes }} + {{- tpl (toYaml .) $root | nindent 2 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/clusterrole.yaml b/charts/devlake/charts/grafana/templates/clusterrole.yaml new file mode 100644 index 0000000..2c9a180 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/clusterrole.yaml @@ -0,0 +1,25 @@ +{{- if and .Values.rbac.create (or (not .Values.rbac.namespaced) .Values.rbac.extraClusterRoleRules) (not .Values.rbac.useExistingRole) }} +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ include "grafana.fullname" . }}-clusterrole +{{- if or .Values.sidecar.dashboards.enabled .Values.rbac.extraClusterRoleRules .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled .Values.sidecar.alerts.enabled }} +rules: + {{- if or .Values.sidecar.dashboards.enabled .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled .Values.sidecar.alerts.enabled }} + - apiGroups: [""] # "" indicates the core API group + resources: ["configmaps", "secrets"] + verbs: ["get", "watch", "list"] + {{- end}} + {{- with .Values.rbac.extraClusterRoleRules }} + {{- toYaml . | nindent 2 }} + {{- end}} +{{- else }} +rules: [] +{{- end}} +{{- end}} diff --git a/charts/devlake/charts/grafana/templates/clusterrolebinding.yaml b/charts/devlake/charts/grafana/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..b848e8c --- /dev/null +++ b/charts/devlake/charts/grafana/templates/clusterrolebinding.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.rbac.create (or (not .Values.rbac.namespaced) .Values.rbac.extraClusterRoleRules) }} +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "grafana.fullname" . }}-clusterrolebinding + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +subjects: + - kind: ServiceAccount + name: {{ include "grafana.serviceAccountName" . }} + namespace: {{ include "grafana.namespace" . }} +roleRef: + kind: ClusterRole + {{- if .Values.rbac.useExistingRole }} + name: {{ .Values.rbac.useExistingRole }} + {{- else }} + name: {{ include "grafana.fullname" . }}-clusterrole + {{- end }} + apiGroup: rbac.authorization.k8s.io +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/configmap-dashboard-provider.yaml b/charts/devlake/charts/grafana/templates/configmap-dashboard-provider.yaml new file mode 100644 index 0000000..1f706a8 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/configmap-dashboard-provider.yaml @@ -0,0 +1,29 @@ +{{- if and .Values.sidecar.dashboards.enabled .Values.sidecar.dashboards.SCProvider }} +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ include "grafana.fullname" . }}-config-dashboards + namespace: {{ include "grafana.namespace" . }} +data: + provider.yaml: |- + apiVersion: 1 + providers: + - name: '{{ .Values.sidecar.dashboards.provider.name }}' + orgId: {{ .Values.sidecar.dashboards.provider.orgid }} + {{- if not .Values.sidecar.dashboards.provider.foldersFromFilesStructure }} + folder: '{{ .Values.sidecar.dashboards.provider.folder }}' + {{- end }} + type: {{ .Values.sidecar.dashboards.provider.type }} + disableDeletion: {{ .Values.sidecar.dashboards.provider.disableDelete }} + allowUiUpdates: {{ .Values.sidecar.dashboards.provider.allowUiUpdates }} + updateIntervalSeconds: {{ .Values.sidecar.dashboards.provider.updateIntervalSeconds | default 30 }} + options: + foldersFromFilesStructure: {{ .Values.sidecar.dashboards.provider.foldersFromFilesStructure }} + path: {{ .Values.sidecar.dashboards.folder }}{{- with .Values.sidecar.dashboards.defaultFolderName }}/{{ . }}{{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/configmap.yaml b/charts/devlake/charts/grafana/templates/configmap.yaml new file mode 100644 index 0000000..b5f21e8 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/configmap.yaml @@ -0,0 +1,138 @@ +{{- if .Values.createConfigmap }} +{{- $files := .Files }} +{{- $root := . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +data: + {{- with .Values.plugins }} + plugins: {{ join "," . }} + {{- end }} + grafana.ini: | + {{- range $elem, $elemVal := index .Values "grafana.ini" }} + {{- if not (kindIs "map" $elemVal) }} + {{- if kindIs "invalid" $elemVal }} + {{ $elem }} = + {{- else if kindIs "string" $elemVal }} + {{ $elem }} = {{ tpl $elemVal $ }} + {{- else }} + {{ $elem }} = {{ $elemVal }} + {{- end }} + {{- end }} + {{- end }} + {{- range $key, $value := index .Values "grafana.ini" }} + {{- if kindIs "map" $value }} + [{{ $key }}] + {{- range $elem, $elemVal := $value }} + {{- if kindIs "invalid" $elemVal }} + {{ $elem }} = + {{- else if kindIs "string" $elemVal }} + {{ $elem }} = {{ tpl $elemVal $ }} + {{- else }} + {{ $elem }} = {{ $elemVal }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + + {{- range $key, $value := .Values.datasources }} + {{- $key | nindent 2 }}: | + {{- tpl (toYaml $value | nindent 4) $root }} + {{- end }} + + {{- range $key, $value := .Values.notifiers }} + {{- $key | nindent 2 }}: | + {{- toYaml $value | nindent 4 }} + {{- end }} + + {{- range $key, $value := .Values.alerting }} + {{- if (hasKey $value "file") }} + {{- $key | nindent 2 }}: + {{- toYaml ( $files.Get $value.file ) | nindent 4}} + {{- else }} + {{- $key | nindent 2 }}: | + {{- tpl (toYaml $value | nindent 4) $root }} + {{- end }} + {{- end }} + + {{- range $key, $value := .Values.dashboardProviders }} + {{- $key | nindent 2 }}: | + {{- toYaml $value | nindent 4 }} + {{- end }} + +{{- if .Values.dashboards }} + download_dashboards.sh: | + #!/usr/bin/env sh + set -euf + {{- if .Values.dashboardProviders }} + {{- range $key, $value := .Values.dashboardProviders }} + {{- range $value.providers }} + mkdir -p {{ .options.path }} + {{- end }} + {{- end }} + {{- end }} + {{ $dashboardProviders := .Values.dashboardProviders }} + {{- range $provider, $dashboards := .Values.dashboards }} + {{- range $key, $value := $dashboards }} + {{- if (or (hasKey $value "gnetId") (hasKey $value "url")) }} + curl -skf \ + --connect-timeout 60 \ + --max-time 60 \ + {{- if not $value.b64content }} + {{- if not $value.acceptHeader }} + -H "Accept: application/json" \ + {{- else }} + -H "Accept: {{ $value.acceptHeader }}" \ + {{- end }} + {{- if $value.token }} + -H "Authorization: token {{ $value.token }}" \ + {{- end }} + {{- if $value.bearerToken }} + -H "Authorization: Bearer {{ $value.bearerToken }}" \ + {{- end }} + {{- if $value.basic }} + -H "Authorization: Basic {{ $value.basic }}" \ + {{- end }} + {{- if $value.gitlabToken }} + -H "PRIVATE-TOKEN: {{ $value.gitlabToken }}" \ + {{- end }} + -H "Content-Type: application/json;charset=UTF-8" \ + {{- end }} + {{- $dpPath := "" -}} + {{- range $kd := (index $dashboardProviders "dashboardproviders.yaml").providers }} + {{- if eq $kd.name $provider }} + {{- $dpPath = $kd.options.path }} + {{- end }} + {{- end }} + {{- if $value.url }} + "{{ $value.url }}" \ + {{- else }} + "https://grafana.com/api/dashboards/{{ $value.gnetId }}/revisions/{{- if $value.revision -}}{{ $value.revision }}{{- else -}}1{{- end -}}/download" \ + {{- end }} + {{- if $value.datasource }} + {{- if kindIs "string" $value.datasource }} + | sed '/-- .* --/! s/"datasource":.*,/"datasource": "{{ $value.datasource }}",/g' \ + {{- end }} + {{- if kindIs "slice" $value.datasource }} + {{- range $value.datasource }} + | sed '/-- .* --/! s/${{"{"}}{{ .name }}}/{{ .value }}/g' \ + {{- end }} + {{- end }} + {{- end }} + {{- if $value.b64content }} + | base64 -d \ + {{- end }} + > "{{- if $dpPath -}}{{ $dpPath }}{{- else -}}/var/lib/grafana/dashboards/{{ $provider }}{{- end -}}/{{ $key }}.json" + {{ end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/dashboards-json-configmap.yaml b/charts/devlake/charts/grafana/templates/dashboards-json-configmap.yaml new file mode 100644 index 0000000..df0ed0d --- /dev/null +++ b/charts/devlake/charts/grafana/templates/dashboards-json-configmap.yaml @@ -0,0 +1,35 @@ +{{- if .Values.dashboards }} +{{ $files := .Files }} +{{- range $provider, $dashboards := .Values.dashboards }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "grafana.fullname" $ }}-dashboards-{{ $provider }} + namespace: {{ include "grafana.namespace" $ }} + labels: + {{- include "grafana.labels" $ | nindent 4 }} + dashboard-provider: {{ $provider }} +{{- if $dashboards }} +data: +{{- $dashboardFound := false }} +{{- range $key, $value := $dashboards }} +{{- if (or (hasKey $value "json") (hasKey $value "file")) }} +{{- $dashboardFound = true }} + {{- print $key | nindent 2 }}.json: + {{- if hasKey $value "json" }} + |- + {{- $value.json | nindent 6 }} + {{- end }} + {{- if hasKey $value "file" }} + {{- toYaml ( $files.Get $value.file ) | nindent 4}} + {{- end }} +{{- end }} +{{- end }} +{{- if not $dashboardFound }} + {} +{{- end }} +{{- end }} +--- +{{- end }} + +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/deployment.yaml b/charts/devlake/charts/grafana/templates/deployment.yaml new file mode 100644 index 0000000..bfa26bb --- /dev/null +++ b/charts/devlake/charts/grafana/templates/deployment.yaml @@ -0,0 +1,51 @@ +{{- if (and (not .Values.useStatefulSet) (or (not .Values.persistence.enabled) (eq .Values.persistence.type "pvc"))) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and (not .Values.autoscaling.enabled) (.Values.replicas) }} + replicas: {{ .Values.replicas }} + {{- end }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + selector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 6 }} + {{- with .Values.deploymentStrategy }} + strategy: + {{- toYaml . | trim | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "grafana.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/dashboards-json-config: {{ include (print $.Template.BasePath "/dashboards-json-configmap.yaml") . | sha256sum }} + checksum/sc-dashboard-provider-config: {{ include (print $.Template.BasePath "/configmap-dashboard-provider.yaml") . | sha256sum }} + {{- if and (or (and (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD)) (and .Values.ldap.enabled (not .Values.ldap.existingSecret))) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.envRenderSecret }} + checksum/secret-env: {{ include (print $.Template.BasePath "/secret-env.yaml") . | sha256sum }} + {{- end }} + kubectl.kubernetes.io/default-container: {{ .Chart.Name }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "grafana.pod" . | nindent 6 }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/extra-manifests.yaml b/charts/devlake/charts/grafana/templates/extra-manifests.yaml new file mode 100644 index 0000000..a9bb3b6 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/extra-manifests.yaml @@ -0,0 +1,4 @@ +{{ range .Values.extraObjects }} +--- +{{ tpl (toYaml .) $ }} +{{ end }} diff --git a/charts/devlake/charts/grafana/templates/headless-service.yaml b/charts/devlake/charts/grafana/templates/headless-service.yaml new file mode 100644 index 0000000..3028589 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/headless-service.yaml @@ -0,0 +1,22 @@ +{{- $sts := list "sts" "StatefulSet" "statefulset" -}} +{{- if or .Values.headlessService (and .Values.persistence.enabled (not .Values.persistence.existingClaim) (has .Values.persistence.type $sts)) }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "grafana.fullname" . }}-headless + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + clusterIP: None + selector: + {{- include "grafana.selectorLabels" . | nindent 4 }} + type: ClusterIP + ports: + - name: {{ .Values.gossipPortName }}-tcp + port: 9094 +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/hpa.yaml b/charts/devlake/charts/grafana/templates/hpa.yaml new file mode 100644 index 0000000..46bbcb4 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/hpa.yaml @@ -0,0 +1,52 @@ +{{- $sts := list "sts" "StatefulSet" "statefulset" -}} +{{- if .Values.autoscaling.enabled }} +apiVersion: {{ include "grafana.hpa.apiVersion" . }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "grafana.name" . }} + helm.sh/chart: {{ include "grafana.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + {{- if has .Values.persistence.type $sts }} + kind: StatefulSet + {{- else }} + kind: Deployment + {{- end }} + name: {{ include "grafana.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetMemory }} + - type: Resource + resource: + name: memory + {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} + targetAverageUtilization: {{ .Values.autoscaling.targetMemory }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemory }} + {{- end }} + {{- end }} + {{- if .Values.autoscaling.targetCPU }} + - type: Resource + resource: + name: cpu + {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} + targetAverageUtilization: {{ .Values.autoscaling.targetCPU }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPU }} + {{- end }} + {{- end }} + {{- if .Values.autoscaling.behavior }} + behavior: {{ toYaml .Values.autoscaling.behavior | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/image-renderer-deployment.yaml b/charts/devlake/charts/grafana/templates/image-renderer-deployment.yaml new file mode 100644 index 0000000..93d20e8 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/image-renderer-deployment.yaml @@ -0,0 +1,130 @@ +{{ if .Values.imageRenderer.enabled }} +{{- $root := . -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "grafana.fullname" . }}-image-renderer + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.imageRenderer.labels" . | nindent 4 }} + {{- with .Values.imageRenderer.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.imageRenderer.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and (not .Values.imageRenderer.autoscaling.enabled) (.Values.imageRenderer.replicas) }} + replicas: {{ .Values.imageRenderer.replicas }} + {{- end }} + revisionHistoryLimit: {{ .Values.imageRenderer.revisionHistoryLimit }} + selector: + matchLabels: + {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} + + {{- with .Values.imageRenderer.deploymentStrategy }} + strategy: + {{- toYaml . | trim | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "grafana.imageRenderer.selectorLabels" . | nindent 8 }} + {{- with .Values.imageRenderer.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.imageRenderer.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imageRenderer.schedulerName }} + schedulerName: "{{ . }}" + {{- end }} + {{- with .Values.imageRenderer.serviceAccountName }} + serviceAccountName: "{{ . }}" + {{- end }} + {{- with .Values.imageRenderer.securityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imageRenderer.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imageRenderer.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- with .Values.imageRenderer.image.pullSecrets }} + imagePullSecrets: + {{- range . }} + - name: {{ tpl . $root }} + {{- end}} + {{- end }} + containers: + - name: {{ .Chart.Name }}-image-renderer + {{- if .Values.imageRenderer.image.sha }} + image: "{{ .Values.imageRenderer.image.repository }}:{{ .Values.imageRenderer.image.tag }}@sha256:{{ .Values.imageRenderer.image.sha }}" + {{- else }} + image: "{{ .Values.imageRenderer.image.repository }}:{{ .Values.imageRenderer.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.imageRenderer.image.pullPolicy }} + {{- if .Values.imageRenderer.command }} + command: + {{- range .Values.imageRenderer.command }} + - {{ . }} + {{- end }} + {{- end}} + ports: + - name: {{ .Values.imageRenderer.service.portName }} + containerPort: {{ .Values.imageRenderer.service.targetPort }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: {{ .Values.imageRenderer.service.portName }} + env: + - name: HTTP_PORT + value: {{ .Values.imageRenderer.service.targetPort | quote }} + {{- if .Values.imageRenderer.serviceMonitor.enabled }} + - name: ENABLE_METRICS + value: "true" + {{- end }} + {{- range $key, $value := .Values.imageRenderer.envValueFrom }} + - name: {{ $key | quote }} + valueFrom: + {{- tpl (toYaml $value) $ | nindent 16 }} + {{- end }} + {{- range $key, $value := .Values.imageRenderer.env }} + - name: {{ $key | quote }} + value: {{ $value | quote }} + {{- end }} + {{- with .Values.imageRenderer.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - mountPath: /tmp + name: image-renderer-tmpfs + {{- with .Values.imageRenderer.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.imageRenderer.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imageRenderer.affinity }} + affinity: + {{- tpl (toYaml .) $root | nindent 8 }} + {{- end }} + {{- with .Values.imageRenderer.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: image-renderer-tmpfs + emptyDir: {} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/image-renderer-hpa.yaml b/charts/devlake/charts/grafana/templates/image-renderer-hpa.yaml new file mode 100644 index 0000000..b0f0059 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/image-renderer-hpa.yaml @@ -0,0 +1,47 @@ +{{- if and .Values.imageRenderer.enabled .Values.imageRenderer.autoscaling.enabled }} +apiVersion: {{ include "grafana.hpa.apiVersion" . }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "grafana.fullname" . }}-image-renderer + namespace: {{ include "grafana.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "grafana.name" . }}-image-renderer + helm.sh/chart: {{ include "grafana.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "grafana.fullname" . }}-image-renderer + minReplicas: {{ .Values.imageRenderer.autoscaling.minReplicas }} + maxReplicas: {{ .Values.imageRenderer.autoscaling.maxReplicas }} + metrics: + {{- if .Values.imageRenderer.autoscaling.targetMemory }} + - type: Resource + resource: + name: memory + {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} + targetAverageUtilization: {{ .Values.imageRenderer.autoscaling.targetMemory }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.imageRenderer.autoscaling.targetMemory }} + {{- end }} + {{- end }} + {{- if .Values.imageRenderer.autoscaling.targetCPU }} + - type: Resource + resource: + name: cpu + {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} + targetAverageUtilization: {{ .Values.imageRenderer.autoscaling.targetCPU }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.imageRenderer.autoscaling.targetCPU }} + {{- end }} + {{- end }} + {{- if .Values.imageRenderer.autoscaling.behavior }} + behavior: {{ toYaml .Values.imageRenderer.autoscaling.behavior | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/image-renderer-network-policy.yaml b/charts/devlake/charts/grafana/templates/image-renderer-network-policy.yaml new file mode 100644 index 0000000..d1a0eb3 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/image-renderer-network-policy.yaml @@ -0,0 +1,79 @@ +{{- if and .Values.imageRenderer.enabled .Values.imageRenderer.networkPolicy.limitIngress }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "grafana.fullname" . }}-image-renderer-ingress + namespace: {{ include "grafana.namespace" . }} + annotations: + comment: Limit image-renderer ingress traffic from grafana +spec: + podSelector: + matchLabels: + {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} + {{- with .Values.imageRenderer.podLabels }} + {{- toYaml . | nindent 6 }} + {{- end }} + + policyTypes: + - Ingress + ingress: + - ports: + - port: {{ .Values.imageRenderer.service.targetPort }} + protocol: TCP + from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ include "grafana.namespace" . }} + podSelector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 14 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 14 }} + {{- end }} + {{- with .Values.imageRenderer.networkPolicy.extraIngressSelectors -}} + {{ toYaml . | nindent 8 }} + {{- end }} +{{- end }} + +{{- if and .Values.imageRenderer.enabled .Values.imageRenderer.networkPolicy.limitEgress }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "grafana.fullname" . }}-image-renderer-egress + namespace: {{ include "grafana.namespace" . }} + annotations: + comment: Limit image-renderer egress traffic to grafana +spec: + podSelector: + matchLabels: + {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} + {{- with .Values.imageRenderer.podLabels }} + {{- toYaml . | nindent 6 }} + {{- end }} + + policyTypes: + - Egress + egress: + # allow dns resolution + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + # talk only to grafana + - ports: + - port: {{ .Values.service.targetPort }} + protocol: TCP + to: + - namespaceSelector: + matchLabels: + name: {{ include "grafana.namespace" . }} + podSelector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 14 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 14 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/image-renderer-service.yaml b/charts/devlake/charts/grafana/templates/image-renderer-service.yaml new file mode 100644 index 0000000..f8da127 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/image-renderer-service.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.imageRenderer.enabled .Values.imageRenderer.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "grafana.fullname" . }}-image-renderer + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.imageRenderer.labels" . | nindent 4 }} + {{- with .Values.imageRenderer.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.imageRenderer.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + {{- with .Values.imageRenderer.service.clusterIP }} + clusterIP: {{ . }} + {{- end }} + ports: + - name: {{ .Values.imageRenderer.service.portName }} + port: {{ .Values.imageRenderer.service.port }} + protocol: TCP + targetPort: {{ .Values.imageRenderer.service.targetPort }} + {{- with .Values.imageRenderer.appProtocol }} + appProtocol: {{ . }} + {{- end }} + selector: + {{- include "grafana.imageRenderer.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/image-renderer-servicemonitor.yaml b/charts/devlake/charts/grafana/templates/image-renderer-servicemonitor.yaml new file mode 100644 index 0000000..5d9f09d --- /dev/null +++ b/charts/devlake/charts/grafana/templates/image-renderer-servicemonitor.yaml @@ -0,0 +1,48 @@ +{{- if .Values.imageRenderer.serviceMonitor.enabled }} +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "grafana.fullname" . }}-image-renderer + {{- if .Values.imageRenderer.serviceMonitor.namespace }} + namespace: {{ tpl .Values.imageRenderer.serviceMonitor.namespace . }} + {{- else }} + namespace: {{ include "grafana.namespace" . }} + {{- end }} + labels: + {{- include "grafana.imageRenderer.labels" . | nindent 4 }} + {{- with .Values.imageRenderer.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: {{ .Values.imageRenderer.service.portName }} + {{- with .Values.imageRenderer.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.imageRenderer.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + honorLabels: true + path: {{ .Values.imageRenderer.serviceMonitor.path }} + scheme: {{ .Values.imageRenderer.serviceMonitor.scheme }} + {{- with .Values.imageRenderer.serviceMonitor.tlsConfig }} + tlsConfig: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.imageRenderer.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + jobLabel: "{{ .Release.Name }}-image-renderer" + selector: + matchLabels: + {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} + namespaceSelector: + matchNames: + - {{ include "grafana.namespace" . }} + {{- with .Values.imageRenderer.serviceMonitor.targetLabels }} + targetLabels: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/ingress.yaml b/charts/devlake/charts/grafana/templates/ingress.yaml new file mode 100644 index 0000000..063cdfa --- /dev/null +++ b/charts/devlake/charts/grafana/templates/ingress.yaml @@ -0,0 +1,78 @@ +{{- if .Values.ingress.enabled -}} +{{- $ingressApiIsStable := eq (include "grafana.ingress.isStable" .) "true" -}} +{{- $ingressSupportsIngressClassName := eq (include "grafana.ingress.supportsIngressClassName" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "grafana.ingress.supportsPathType" .) "true" -}} +{{- $fullName := include "grafana.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +{{- $ingressPathType := .Values.ingress.pathType -}} +{{- $extraPaths := .Values.ingress.extraPaths -}} +apiVersion: {{ include "grafana.ingress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ tpl $value $ | quote }} + {{- end }} + {{- end }} +spec: + {{- if and $ingressSupportsIngressClassName .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + {{- end -}} + {{- with .Values.ingress.tls }} + tls: + {{- tpl (toYaml .) $ | nindent 4 }} + {{- end }} + rules: + {{- if .Values.ingress.hosts }} + {{- range .Values.ingress.hosts }} + - host: {{ tpl . $ }} + http: + paths: + {{- with $extraPaths }} + {{- toYaml . | nindent 10 }} + {{- end }} + - path: {{ $ingressPath }} + {{- if $ingressSupportsPathType }} + pathType: {{ $ingressPathType }} + {{- end }} + backend: + {{- if $ingressApiIsStable }} + service: + name: {{ $fullName }} + port: + number: {{ $servicePort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $servicePort }} + {{- end }} + {{- end }} + {{- else }} + - http: + paths: + - backend: + {{- if $ingressApiIsStable }} + service: + name: {{ $fullName }} + port: + number: {{ $servicePort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $servicePort }} + {{- end }} + {{- with $ingressPath }} + path: {{ . }} + {{- end }} + {{- if $ingressSupportsPathType }} + pathType: {{ $ingressPathType }} + {{- end }} + {{- end -}} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/networkpolicy.yaml b/charts/devlake/charts/grafana/templates/networkpolicy.yaml new file mode 100644 index 0000000..ea4578b --- /dev/null +++ b/charts/devlake/charts/grafana/templates/networkpolicy.yaml @@ -0,0 +1,52 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + policyTypes: + {{- if .Values.networkPolicy.ingress }} + - Ingress + {{- end }} + {{- if .Values.networkPolicy.egress.enabled }} + - Egress + {{- end }} + podSelector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 6 }} + + {{- if .Values.networkPolicy.egress.enabled }} + egress: + - ports: + {{ .Values.networkPolicy.egress.ports | toJson }} + {{- end }} + {{- if .Values.networkPolicy.ingress }} + ingress: + - ports: + - port: {{ .Values.service.targetPort }} + {{- if not .Values.networkPolicy.allowExternal }} + from: + - podSelector: + matchLabels: + {{ include "grafana.fullname" . }}-client: "true" + {{- with .Values.networkPolicy.explicitNamespacesSelector }} + - namespaceSelector: + {{- toYaml . | nindent 12 }} + {{- end }} + - podSelector: + matchLabels: + {{- include "grafana.labels" . | nindent 14 }} + role: read + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/poddisruptionbudget.yaml b/charts/devlake/charts/grafana/templates/poddisruptionbudget.yaml new file mode 100644 index 0000000..0525121 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/poddisruptionbudget.yaml @@ -0,0 +1,22 @@ +{{- if .Values.podDisruptionBudget }} +apiVersion: {{ include "grafana.podDisruptionBudget.apiVersion" . }} +kind: PodDisruptionBudget +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ . }} + {{- end }} + {{- with .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ . }} + {{- end }} + selector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/podsecuritypolicy.yaml b/charts/devlake/charts/grafana/templates/podsecuritypolicy.yaml new file mode 100644 index 0000000..eed7af9 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/podsecuritypolicy.yaml @@ -0,0 +1,49 @@ +{{- if and .Values.rbac.pspEnabled (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ include "grafana.fullname" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default' + seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default' + {{- if .Values.rbac.pspUseAppArmor }} + apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default' + apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default' + {{- end }} +spec: + privileged: false + allowPrivilegeEscalation: false + requiredDropCapabilities: + # Default set from Docker, with DAC_OVERRIDE and CHOWN + - ALL + volumes: + - 'configMap' + - 'emptyDir' + - 'projected' + - 'csi' + - 'secret' + - 'downwardAPI' + - 'persistentVolumeClaim' + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'MustRunAs' + ranges: + # Forbid adding the root group. + - min: 1 + max: 65535 + fsGroup: + rule: 'MustRunAs' + ranges: + # Forbid adding the root group. + - min: 1 + max: 65535 + readOnlyRootFilesystem: false +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/pvc.yaml b/charts/devlake/charts/grafana/templates/pvc.yaml new file mode 100644 index 0000000..eb8f87f --- /dev/null +++ b/charts/devlake/charts/grafana/templates/pvc.yaml @@ -0,0 +1,36 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) (eq .Values.persistence.type "pvc")}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.persistence.extraPvcLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.persistence.finalizers }} + finalizers: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range .Values.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- with .Values.persistence.storageClassName }} + storageClassName: {{ . }} + {{- end }} + {{- with .Values.persistence.selectorLabels }} + selector: + matchLabels: + {{- toYaml . | nindent 6 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/role.yaml b/charts/devlake/charts/grafana/templates/role.yaml new file mode 100644 index 0000000..df8ac9a --- /dev/null +++ b/charts/devlake/charts/grafana/templates/role.yaml @@ -0,0 +1,32 @@ +{{- if and .Values.rbac.create (not .Values.rbac.useExistingRole) -}} +apiVersion: {{ include "grafana.rbac.apiVersion" . }} +kind: Role +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- if or .Values.rbac.pspEnabled (and .Values.rbac.namespaced (or .Values.sidecar.dashboards.enabled .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled .Values.rbac.extraRoleRules)) }} +rules: + {{- if and .Values.rbac.pspEnabled (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") }} + - apiGroups: ['extensions'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ include "grafana.fullname" . }}] + {{- end }} + {{- if and .Values.rbac.namespaced (or .Values.sidecar.dashboards.enabled .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled) }} + - apiGroups: [""] # "" indicates the core API group + resources: ["configmaps", "secrets"] + verbs: ["get", "watch", "list"] + {{- end }} + {{- with .Values.rbac.extraRoleRules }} + {{- toYaml . | nindent 2 }} + {{- end}} +{{- else }} +rules: [] +{{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/rolebinding.yaml b/charts/devlake/charts/grafana/templates/rolebinding.yaml new file mode 100644 index 0000000..cc07bd9 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/rolebinding.yaml @@ -0,0 +1,25 @@ +{{- if .Values.rbac.create }} +apiVersion: {{ include "grafana.rbac.apiVersion" . }} +kind: RoleBinding +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + {{- if .Values.rbac.useExistingRole }} + name: {{ .Values.rbac.useExistingRole }} + {{- else }} + name: {{ include "grafana.fullname" . }} + {{- end }} +subjects: +- kind: ServiceAccount + name: {{ include "grafana.serviceAccountName" . }} + namespace: {{ include "grafana.namespace" . }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/secret-env.yaml b/charts/devlake/charts/grafana/templates/secret-env.yaml new file mode 100644 index 0000000..c765567 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/secret-env.yaml @@ -0,0 +1,14 @@ +{{- if .Values.envRenderSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "grafana.fullname" . }}-env + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} +type: Opaque +data: +{{- range $key, $val := .Values.envRenderSecret }} + {{ $key }}: {{ $val | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/secret.yaml b/charts/devlake/charts/grafana/templates/secret.yaml new file mode 100644 index 0000000..5cbd527 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/secret.yaml @@ -0,0 +1,26 @@ +{{- if or (and (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION)) (and .Values.ldap.enabled (not .Values.ldap.existingSecret)) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +type: Opaque +data: + {{- if and (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) }} + admin-user: {{ .Values.adminUser | b64enc | quote }} + {{- if .Values.adminPassword }} + admin-password: {{ .Values.adminPassword | b64enc | quote }} + {{- else }} + admin-password: {{ include "grafana.password" . }} + {{- end }} + {{- end }} + {{- if not .Values.ldap.existingSecret }} + ldap-toml: {{ tpl .Values.ldap.config $ | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/service.yaml b/charts/devlake/charts/grafana/templates/service.yaml new file mode 100644 index 0000000..43d360b --- /dev/null +++ b/charts/devlake/charts/grafana/templates/service.yaml @@ -0,0 +1,55 @@ +{{- if .Values.service.enabled }} +{{- $root := . }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.service.annotations }} + annotations: + {{- tpl (toYaml . | nindent 4) $root }} + {{- end }} +spec: + {{- if (or (eq .Values.service.type "ClusterIP") (empty .Values.service.type)) }} + type: ClusterIP + {{- with .Values.service.clusterIP }} + clusterIP: {{ . }} + {{- end }} + {{- else if eq .Values.service.type "LoadBalancer" }} + type: {{ .Values.service.type }} + {{- with .Values.service.loadBalancerIP }} + loadBalancerIP: {{ . }} + {{- end }} + {{- with .Values.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- else }} + type: {{ .Values.service.type }} + {{- end }} + {{- with .Values.service.externalIPs }} + externalIPs: + {{- toYaml . | nindent 4 }} + {{- end }} + ports: + - name: {{ .Values.service.portName }} + port: {{ .Values.service.port }} + protocol: TCP + targetPort: {{ .Values.service.targetPort }} + {{- with .Values.service.appProtocol }} + appProtocol: {{ . }} + {{- end }} + {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + {{- with .Values.extraExposePorts }} + {{- tpl (toYaml . | nindent 4) $root }} + {{- end }} + selector: + {{- include "grafana.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/serviceaccount.yaml b/charts/devlake/charts/grafana/templates/serviceaccount.yaml new file mode 100644 index 0000000..784e71b --- /dev/null +++ b/charts/devlake/charts/grafana/templates/serviceaccount.yaml @@ -0,0 +1,17 @@ +{{- if .Values.serviceAccount.create }} +{{- $root := . -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- tpl (toYaml . | nindent 4) $root }} + {{- end }} + name: {{ include "grafana.serviceAccountName" . }} + namespace: {{ include "grafana.namespace" . }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/servicemonitor.yaml b/charts/devlake/charts/grafana/templates/servicemonitor.yaml new file mode 100644 index 0000000..a4e9f00 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/servicemonitor.yaml @@ -0,0 +1,48 @@ +{{- if .Values.serviceMonitor.enabled }} +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "grafana.fullname" . }} + {{- if .Values.serviceMonitor.namespace }} + namespace: {{ tpl .Values.serviceMonitor.namespace . }} + {{- else }} + namespace: {{ include "grafana.namespace" . }} + {{- end }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: {{ .Values.service.portName }} + {{- with .Values.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + honorLabels: true + path: {{ .Values.serviceMonitor.path }} + scheme: {{ .Values.serviceMonitor.scheme }} + {{- with .Values.serviceMonitor.tlsConfig }} + tlsConfig: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + jobLabel: "{{ .Release.Name }}" + selector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 6 }} + namespaceSelector: + matchNames: + - {{ include "grafana.namespace" . }} + {{- with .Values.serviceMonitor.targetLabels }} + targetLabels: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/statefulset.yaml b/charts/devlake/charts/grafana/templates/statefulset.yaml new file mode 100644 index 0000000..e6c944a --- /dev/null +++ b/charts/devlake/charts/grafana/templates/statefulset.yaml @@ -0,0 +1,56 @@ +{{- $sts := list "sts" "StatefulSet" "statefulset" -}} +{{- if (or (.Values.useStatefulSet) (and .Values.persistence.enabled (not .Values.persistence.existingClaim) (has .Values.persistence.type $sts)))}} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "grafana.fullname" . }} + namespace: {{ include "grafana.namespace" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + {{- include "grafana.selectorLabels" . | nindent 6 }} + serviceName: {{ include "grafana.fullname" . }}-headless + template: + metadata: + labels: + {{- include "grafana.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/dashboards-json-config: {{ include (print $.Template.BasePath "/dashboards-json-configmap.yaml") . | sha256sum }} + checksum/sc-dashboard-provider-config: {{ include (print $.Template.BasePath "/configmap-dashboard-provider.yaml") . | sha256sum }} + {{- if and (or (and (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD)) (and .Values.ldap.enabled (not .Values.ldap.existingSecret))) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + kubectl.kubernetes.io/default-container: {{ .Chart.Name }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "grafana.pod" . | nindent 6 }} + {{- if .Values.persistence.enabled}} + volumeClaimTemplates: + - metadata: + name: storage + spec: + accessModes: {{ .Values.persistence.accessModes }} + storageClassName: {{ .Values.persistence.storageClassName }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- with .Values.persistence.selectorLabels }} + selector: + matchLabels: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/tests/test-configmap.yaml b/charts/devlake/charts/grafana/templates/tests/test-configmap.yaml new file mode 100644 index 0000000..01c96c9 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/tests/test-configmap.yaml @@ -0,0 +1,20 @@ +{{- if .Values.testFramework.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "grafana.fullname" . }}-test + namespace: {{ include "grafana.namespace" . }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" + labels: + {{- include "grafana.labels" . | nindent 4 }} +data: + run.sh: |- + @test "Test Health" { + url="http://{{ include "grafana.fullname" . }}/api/health" + + code=$(wget --server-response --spider --timeout 90 --tries 10 ${url} 2>&1 | awk '/^ HTTP/{print $2}') + [ "$code" == "200" ] + } +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/tests/test-podsecuritypolicy.yaml b/charts/devlake/charts/grafana/templates/tests/test-podsecuritypolicy.yaml new file mode 100644 index 0000000..1821772 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/tests/test-podsecuritypolicy.yaml @@ -0,0 +1,32 @@ +{{- if and (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") .Values.testFramework.enabled .Values.rbac.pspEnabled }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ include "grafana.fullname" . }}-test + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" + labels: + {{- include "grafana.labels" . | nindent 4 }} +spec: + allowPrivilegeEscalation: true + privileged: false + hostNetwork: false + hostIPC: false + hostPID: false + fsGroup: + rule: RunAsAny + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + volumes: + - configMap + - downwardAPI + - emptyDir + - projected + - csi + - secret +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/tests/test-role.yaml b/charts/devlake/charts/grafana/templates/tests/test-role.yaml new file mode 100644 index 0000000..cb4c782 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/tests/test-role.yaml @@ -0,0 +1,17 @@ +{{- if and (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") .Values.testFramework.enabled .Values.rbac.pspEnabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "grafana.fullname" . }}-test + namespace: {{ include "grafana.namespace" . }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" + labels: + {{- include "grafana.labels" . | nindent 4 }} +rules: + - apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ include "grafana.fullname" . }}-test] +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/tests/test-rolebinding.yaml b/charts/devlake/charts/grafana/templates/tests/test-rolebinding.yaml new file mode 100644 index 0000000..f40d791 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/tests/test-rolebinding.yaml @@ -0,0 +1,20 @@ +{{- if and (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") .Values.testFramework.enabled .Values.rbac.pspEnabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "grafana.fullname" . }}-test + namespace: {{ include "grafana.namespace" . }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" + labels: + {{- include "grafana.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "grafana.fullname" . }}-test +subjects: + - kind: ServiceAccount + name: {{ include "grafana.serviceAccountNameTest" . }} + namespace: {{ include "grafana.namespace" . }} +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/tests/test-serviceaccount.yaml b/charts/devlake/charts/grafana/templates/tests/test-serviceaccount.yaml new file mode 100644 index 0000000..38fba35 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/tests/test-serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if and .Values.testFramework.enabled .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + {{- include "grafana.labels" . | nindent 4 }} + name: {{ include "grafana.serviceAccountNameTest" . }} + namespace: {{ include "grafana.namespace" . }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" +{{- end }} diff --git a/charts/devlake/charts/grafana/templates/tests/test.yaml b/charts/devlake/charts/grafana/templates/tests/test.yaml new file mode 100644 index 0000000..9fb8842 --- /dev/null +++ b/charts/devlake/charts/grafana/templates/tests/test.yaml @@ -0,0 +1,49 @@ +{{- if .Values.testFramework.enabled }} +{{- $root := . }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "grafana.fullname" . }}-test + labels: + {{- include "grafana.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" + namespace: {{ include "grafana.namespace" . }} +spec: + serviceAccountName: {{ include "grafana.serviceAccountNameTest" . }} + {{- with .Values.testFramework.securityContext }} + securityContext: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if or .Values.image.pullSecrets .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- include "grafana.imagePullSecrets" (dict "root" $root "imagePullSecrets" .Values.image.pullSecrets) | nindent 4 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- tpl (toYaml .) $root | nindent 4 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 4 }} + {{- end }} + containers: + - name: {{ .Release.Name }}-test + image: "{{ .Values.testFramework.image}}:{{ .Values.testFramework.tag }}" + imagePullPolicy: "{{ .Values.testFramework.imagePullPolicy}}" + command: ["/opt/bats/bin/bats", "-t", "/tests/run.sh"] + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + volumes: + - name: tests + configMap: + name: {{ include "grafana.fullname" . }}-test + restartPolicy: Never +{{- end }} diff --git a/charts/devlake/charts/grafana/values.yaml b/charts/devlake/charts/grafana/values.yaml new file mode 100644 index 0000000..a6d8aec --- /dev/null +++ b/charts/devlake/charts/grafana/values.yaml @@ -0,0 +1,1240 @@ +global: + # To help compatibility with other charts which use global.imagePullSecrets. + # Allow either an array of {name: pullSecret} maps (k8s-style), or an array of strings (more common helm-style). + # Can be tempalted. + # global: + # imagePullSecrets: + # - name: pullSecret1 + # - name: pullSecret2 + # or + # global: + # imagePullSecrets: + # - pullSecret1 + # - pullSecret2 + imagePullSecrets: [] + +rbac: + create: true + ## Use an existing ClusterRole/Role (depending on rbac.namespaced false/true) + # useExistingRole: name-of-some-(cluster)role + pspEnabled: false + pspUseAppArmor: false + namespaced: false + extraRoleRules: [] + # - apiGroups: [] + # resources: [] + # verbs: [] + extraClusterRoleRules: [] + # - apiGroups: [] + # resources: [] + # verbs: [] +serviceAccount: + create: true + name: + nameTest: + ## ServiceAccount labels. + labels: {} +## Service account annotations. Can be templated. +# annotations: +# eks.amazonaws.com/role-arn: arn:aws:iam::123456789000:role/iam-role-name-here + autoMount: true + +replicas: 1 + +## Create a headless service for the deployment +headlessService: false + +## Create HorizontalPodAutoscaler object for deployment type +# +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPU: "60" + targetMemory: "" + behavior: {} + +## See `kubectl explain poddisruptionbudget.spec` for more +## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ +podDisruptionBudget: {} +# minAvailable: 1 +# maxUnavailable: 1 + +## See `kubectl explain deployment.spec.strategy` for more +## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy +deploymentStrategy: + type: RollingUpdate + +readinessProbe: + httpGet: + path: /api/health + port: 3000 + +livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 60 + timeoutSeconds: 30 + failureThreshold: 10 + +## Use an alternate scheduler, e.g. "stork". +## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ +## +# schedulerName: "default-scheduler" + +image: + repository: docker.io/grafana/grafana + # Overrides the Grafana image tag whose default is the chart appVersion + tag: "" + sha: "" + pullPolicy: IfNotPresent + + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## Can be templated. + ## + pullSecrets: [] + # - myRegistrKeySecretName + +testFramework: + enabled: true + image: docker.io/bats/bats + tag: "v1.4.1" + imagePullPolicy: IfNotPresent + securityContext: {} + +securityContext: + runAsNonRoot: true + runAsUser: 472 + runAsGroup: 472 + fsGroup: 472 + +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +# Enable creating the grafana configmap +createConfigmap: true + +# Extra configmaps to mount in grafana pods +# Values are templated. +extraConfigmapMounts: [] + # - name: certs-configmap + # mountPath: /etc/grafana/ssl/ + # subPath: certificates.crt # (optional) + # configMap: certs-configmap + # readOnly: true + + +extraEmptyDirMounts: [] + # - name: provisioning-notifiers + # mountPath: /etc/grafana/provisioning/notifiers + + +# Apply extra labels to common labels. +extraLabels: {} + +## Assign a PriorityClassName to pods if set +# priorityClassName: + +downloadDashboardsImage: + repository: docker.io/curlimages/curl + tag: 7.85.0 + sha: "" + pullPolicy: IfNotPresent + +downloadDashboards: + env: {} + envFromSecret: "" + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + envValueFrom: {} + # ENV_NAME: + # configMapKeyRef: + # name: configmap-name + # key: value_key + +## Pod Annotations +# podAnnotations: {} + +## Pod Labels +# podLabels: {} + +podPortName: grafana +gossipPortName: gossip +## Deployment annotations +# annotations: {} + +## Expose the grafana service to be accessed from outside the cluster (LoadBalancer service). +## or access it from within the cluster (ClusterIP service). Set the service type and the port to serve it. +## ref: http://kubernetes.io/docs/user-guide/services/ +## +service: + enabled: true + type: ClusterIP + port: 80 + targetPort: 3000 + # targetPort: 4181 To be used with a proxy extraContainer + ## Service annotations. Can be templated. + annotations: {} + labels: {} + portName: service + # Adds the appProtocol field to the service. This allows to work with istio protocol selection. Ex: "http" or "tcp" + appProtocol: "" + +serviceMonitor: + ## If true, a ServiceMonitor CRD is created for a prometheus operator + ## https://github.com/coreos/prometheus-operator + ## + enabled: false + path: /metrics + # namespace: monitoring (defaults to use the namespace this chart is deployed to) + labels: {} + interval: 1m + scheme: http + tlsConfig: {} + scrapeTimeout: 30s + relabelings: [] + targetLabels: [] + +extraExposePorts: [] + # - name: keycloak + # port: 8080 + # targetPort: 8080 + # type: ClusterIP + +# overrides pod.spec.hostAliases in the grafana deployment's pods +hostAliases: [] + # - ip: "1.2.3.4" + # hostnames: + # - "my.host.com" + +ingress: + enabled: false + # For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName + # See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress + # ingressClassName: nginx + # Values can be templated + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + labels: {} + path: / + + # pathType is only for k8s >= 1.1= + pathType: Prefix + + hosts: + - chart-example.local + ## Extra paths to prepend to every host configuration. This is useful when working with annotation based services. + extraPaths: [] + # - path: /* + # backend: + # serviceName: ssl-redirect + # servicePort: use-annotation + ## Or for k8s > 1.19 + # - path: /* + # pathType: Prefix + # backend: + # service: + # name: ssl-redirect + # port: + # name: use-annotation + + + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} +# limits: +# cpu: 100m +# memory: 128Mi +# requests: +# cpu: 100m +# memory: 128Mi + +## Node labels for pod assignment +## ref: https://kubernetes.io/docs/user-guide/node-selection/ +# +nodeSelector: {} + +## Tolerations for pod assignment +## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Affinity for pod assignment (evaluated as template) +## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} + +## Topology Spread Constraints +## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ +## +topologySpreadConstraints: [] + +## Additional init containers (evaluated as template) +## ref: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ +## +extraInitContainers: [] + +## Enable an Specify container in extraContainers. This is meant to allow adding an authentication proxy to a grafana pod +extraContainers: "" +# extraContainers: | +# - name: proxy +# image: quay.io/gambol99/keycloak-proxy:latest +# args: +# - -provider=github +# - -client-id= +# - -client-secret= +# - -github-org= +# - -email-domain=* +# - -cookie-secret= +# - -http-address=http://0.0.0.0:4181 +# - -upstream-url=http://127.0.0.1:3000 +# ports: +# - name: proxy-web +# containerPort: 4181 + +## Volumes that can be used in init containers that will not be mounted to deployment pods +extraContainerVolumes: [] +# - name: volume-from-secret +# secret: +# secretName: secret-to-mount +# - name: empty-dir-volume +# emptyDir: {} + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + type: pvc + enabled: false + # storageClassName: default + accessModes: + - ReadWriteOnce + size: 10Gi + # annotations: {} + finalizers: + - kubernetes.io/pvc-protection + # selectorLabels: {} + ## Sub-directory of the PV to mount. Can be templated. + # subPath: "" + ## Name of an existing PVC. Can be templated. + # existingClaim: + ## Extra labels to apply to a PVC. + extraPvcLabels: {} + + ## If persistence is not enabled, this allows to mount the + ## local storage in-memory to improve performance + ## + inMemory: + enabled: false + ## The maximum usage on memory medium EmptyDir would be + ## the minimum value between the SizeLimit specified + ## here and the sum of memory limits of all containers in a pod + ## + # sizeLimit: 300Mi + +initChownData: + ## If false, data ownership will not be reset at startup + ## This allows the grafana-server to be run with an arbitrary user + ## + enabled: true + + ## initChownData container image + ## + image: + repository: docker.io/library/busybox + tag: "1.31.1" + sha: "" + pullPolicy: IfNotPresent + + ## initChownData resource requests and limits + ## Ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + securityContext: + runAsNonRoot: false + runAsUser: 0 + seccompProfile: + type: RuntimeDefault + capabilities: + add: + - CHOWN + +# Administrator credentials when not using an existing secret (see below) +adminUser: admin +# adminPassword: strongpassword + +# Use an existing secret for the admin user. +admin: + ## Name of the secret. Can be templated. + existingSecret: "" + userKey: admin-user + passwordKey: admin-password + +## Define command to be executed at startup by grafana container +## Needed if using `vault-env` to manage secrets (ref: https://banzaicloud.com/blog/inject-secrets-into-pods-vault/) +## Default is "run.sh" as defined in grafana's Dockerfile +# command: +# - "sh" +# - "/run.sh" + +## Optionally define args if command is used +## Needed if using `hashicorp/envconsul` to manage secrets +## By default no arguments are set +# args: +# - "-secret" +# - "secret/grafana" +# - "./grafana" + +## Extra environment variables that will be pass onto deployment pods +## +## to provide grafana with access to CloudWatch on AWS EKS: +## 1. create an iam role of type "Web identity" with provider oidc.eks.* (note the provider for later) +## 2. edit the "Trust relationships" of the role, add a line inside the StringEquals clause using the +## same oidc eks provider as noted before (same as the existing line) +## also, replace NAMESPACE and prometheus-operator-grafana with the service account namespace and name +## +## "oidc.eks.us-east-1.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:sub": "system:serviceaccount:NAMESPACE:prometheus-operator-grafana", +## +## 3. attach a policy to the role, you can use a built in policy called CloudWatchReadOnlyAccess +## 4. use the following env: (replace 123456789000 and iam-role-name-here with your aws account number and role name) +## +## env: +## AWS_ROLE_ARN: arn:aws:iam::123456789000:role/iam-role-name-here +## AWS_WEB_IDENTITY_TOKEN_FILE: /var/run/secrets/eks.amazonaws.com/serviceaccount/token +## AWS_REGION: us-east-1 +## +## 5. uncomment the EKS section in extraSecretMounts: below +## 6. uncomment the annotation section in the serviceAccount: above +## make sure to replace arn:aws:iam::123456789000:role/iam-role-name-here with your role arn + +env: {} + +## "valueFrom" environment variable references that will be added to deployment pods. Name is templated. +## ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#envvarsource-v1-core +## Renders in container spec as: +## env: +## ... +## - name: +## valueFrom: +## +envValueFrom: {} + # ENV_NAME: + # configMapKeyRef: + # name: configmap-name + # key: value_key + +## The name of a secret in the same kubernetes namespace which contain values to be added to the environment +## This can be useful for auth tokens, etc. Value is templated. +envFromSecret: "" + +## Sensible environment variables that will be rendered as new secret object +## This can be useful for auth tokens, etc +envRenderSecret: {} + +## The names of secrets in the same kubernetes namespace which contain values to be added to the environment +## Each entry should contain a name key, and can optionally specify whether the secret must be defined with an optional key. +## Name is templated. +envFromSecrets: [] +## - name: secret-name +## optional: true + +## The names of conifgmaps in the same kubernetes namespace which contain values to be added to the environment +## Each entry should contain a name key, and can optionally specify whether the configmap must be defined with an optional key. +## Name is templated. +## ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#configmapenvsource-v1-core +envFromConfigMaps: [] +## - name: configmap-name +## optional: true + +# Inject Kubernetes services as environment variables. +# See https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/#environment-variables +enableServiceLinks: true + +## Additional grafana server secret mounts +# Defines additional mounts with secrets. Secrets must be manually created in the namespace. +extraSecretMounts: [] + # - name: secret-files + # mountPath: /etc/secrets + # secretName: grafana-secret-files + # readOnly: true + # subPath: "" + # + # for AWS EKS (cloudwatch) use the following (see also instruction in env: above) + # - name: aws-iam-token + # mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount + # readOnly: true + # projected: + # defaultMode: 420 + # sources: + # - serviceAccountToken: + # audience: sts.amazonaws.com + # expirationSeconds: 86400 + # path: token + # + # for CSI e.g. Azure Key Vault use the following + # - name: secrets-store-inline + # mountPath: /run/secrets + # readOnly: true + # csi: + # driver: secrets-store.csi.k8s.io + # readOnly: true + # volumeAttributes: + # secretProviderClass: "akv-grafana-spc" + # nodePublishSecretRef: # Only required when using service principal mode + # name: grafana-akv-creds # Only required when using service principal mode + +## Additional grafana server volume mounts +# Defines additional volume mounts. +extraVolumeMounts: [] + # - name: extra-volume-0 + # mountPath: /mnt/volume0 + # readOnly: true + # existingClaim: volume-claim + # - name: extra-volume-1 + # mountPath: /mnt/volume1 + # readOnly: true + # hostPath: /usr/shared/ + # - name: grafana-secrets + # csi: true + # data: + # driver: secrets-store.csi.k8s.io + # readOnly: true + # volumeAttributes: + # secretProviderClass: "grafana-env-spc" + +## Container Lifecycle Hooks. Execute a specific bash command or make an HTTP request +lifecycleHooks: {} + # postStart: + # exec: + # command: [] + +## Pass the plugins you want installed as a list. +## +plugins: [] + # - digrich-bubblechart-panel + # - grafana-clock-panel + ## You can also use other plugin download URL, as long as they are valid zip files, + ## and specify the name of the plugin after the semicolon. Like this: + # - https://grafana.com/api/plugins/marcusolsson-json-datasource/versions/1.3.2/download;marcusolsson-json-datasource + +## Configure grafana datasources +## ref: http://docs.grafana.org/administration/provisioning/#datasources +## +datasources: {} +# datasources.yaml: +# apiVersion: 1 +# datasources: +# - name: Prometheus +# type: prometheus +# url: http://prometheus-prometheus-server +# access: proxy +# isDefault: true +# - name: CloudWatch +# type: cloudwatch +# access: proxy +# uid: cloudwatch +# editable: false +# jsonData: +# authType: default +# defaultRegion: us-east-1 + +## Configure grafana alerting (can be templated) +## ref: http://docs.grafana.org/administration/provisioning/#alerting +## +alerting: {} + # rules.yaml: + # apiVersion: 1 + # groups: + # - orgId: 1 + # name: '{{ .Chart.Name }}_my_rule_group' + # folder: my_first_folder + # interval: 60s + # rules: + # - uid: my_id_1 + # title: my_first_rule + # condition: A + # data: + # - refId: A + # datasourceUid: '-100' + # model: + # conditions: + # - evaluator: + # params: + # - 3 + # type: gt + # operator: + # type: and + # query: + # params: + # - A + # reducer: + # type: last + # type: query + # datasource: + # type: __expr__ + # uid: '-100' + # expression: 1==0 + # intervalMs: 1000 + # maxDataPoints: 43200 + # refId: A + # type: math + # dashboardUid: my_dashboard + # panelId: 123 + # noDataState: Alerting + # for: 60s + # annotations: + # some_key: some_value + # labels: + # team: sre_team_1 + # contactpoints.yaml: + # apiVersion: 1 + # contactPoints: + # - orgId: 1 + # name: cp_1 + # receivers: + # - uid: first_uid + # type: pagerduty + # settings: + # integrationKey: XXX + # severity: critical + # class: ping failure + # component: Grafana + # group: app-stack + # summary: | + # {{ `{{ include "default.message" . }}` }} + +## Configure notifiers +## ref: http://docs.grafana.org/administration/provisioning/#alert-notification-channels +## +notifiers: {} +# notifiers.yaml: +# notifiers: +# - name: email-notifier +# type: email +# uid: email1 +# # either: +# org_id: 1 +# # or +# org_name: Main Org. +# is_default: true +# settings: +# addresses: an_email_address@example.com +# delete_notifiers: + +## Configure grafana dashboard providers +## ref: http://docs.grafana.org/administration/provisioning/#dashboards +## +## `path` must be /var/lib/grafana/dashboards/ +## +dashboardProviders: {} +# dashboardproviders.yaml: +# apiVersion: 1 +# providers: +# - name: 'default' +# orgId: 1 +# folder: '' +# type: file +# disableDeletion: false +# editable: true +# options: +# path: /var/lib/grafana/dashboards/default + +## Configure grafana dashboard to import +## NOTE: To use dashboards you must also enable/configure dashboardProviders +## ref: https://grafana.com/dashboards +## +## dashboards per provider, use provider name as key. +## +dashboards: {} + # default: + # some-dashboard: + # json: | + # $RAW_JSON + # custom-dashboard: + # file: dashboards/custom-dashboard.json + # prometheus-stats: + # gnetId: 2 + # revision: 2 + # datasource: Prometheus + # local-dashboard: + # url: https://example.com/repository/test.json + # token: '' + # local-dashboard-base64: + # url: https://example.com/repository/test-b64.json + # token: '' + # b64content: true + # local-dashboard-gitlab: + # url: https://example.com/repository/test-gitlab.json + # gitlabToken: '' + # local-dashboard-bitbucket: + # url: https://example.com/repository/test-bitbucket.json + # bearerToken: '' + # local-dashboard-azure: + # url: https://example.com/repository/test-azure.json + # basic: '' + # acceptHeader: '*/*' + +## Reference to external ConfigMap per provider. Use provider name as key and ConfigMap name as value. +## A provider dashboards must be defined either by external ConfigMaps or in values.yaml, not in both. +## ConfigMap data example: +## +## data: +## example-dashboard.json: | +## RAW_JSON +## +dashboardsConfigMaps: {} +# default: "" + +## Grafana's primary configuration +## NOTE: values in map will be converted to ini format +## ref: http://docs.grafana.org/installation/configuration/ +## +grafana.ini: + paths: + data: /var/lib/grafana/ + logs: /var/log/grafana + plugins: /var/lib/grafana/plugins + provisioning: /etc/grafana/provisioning + analytics: + check_for_updates: true + log: + mode: console + grafana_net: + url: https://grafana.net + server: + domain: "{{ if (and .Values.ingress.enabled .Values.ingress.hosts) }}{{ .Values.ingress.hosts | first }}{{ else }}''{{ end }}" +## grafana Authentication can be enabled with the following values on grafana.ini + # server: + # The full public facing url you use in browser, used for redirects and emails + # root_url: + # https://grafana.com/docs/grafana/latest/auth/github/#enable-github-in-grafana + # auth.github: + # enabled: false + # allow_sign_up: false + # scopes: user:email,read:org + # auth_url: https://github.com/login/oauth/authorize + # token_url: https://github.com/login/oauth/access_token + # api_url: https://api.github.com/user + # team_ids: + # allowed_organizations: + # client_id: + # client_secret: +## LDAP Authentication can be enabled with the following values on grafana.ini +## NOTE: Grafana will fail to start if the value for ldap.toml is invalid + # auth.ldap: + # enabled: true + # allow_sign_up: true + # config_file: /etc/grafana/ldap.toml + +## Grafana's LDAP configuration +## Templated by the template in _helpers.tpl +## NOTE: To enable the grafana.ini must be configured with auth.ldap.enabled +## ref: http://docs.grafana.org/installation/configuration/#auth-ldap +## ref: http://docs.grafana.org/installation/ldap/#configuration +ldap: + enabled: false + # `existingSecret` is a reference to an existing secret containing the ldap configuration + # for Grafana in a key `ldap-toml`. + existingSecret: "" + # `config` is the content of `ldap.toml` that will be stored in the created secret + config: "" + # config: |- + # verbose_logging = true + + # [[servers]] + # host = "my-ldap-server" + # port = 636 + # use_ssl = true + # start_tls = false + # ssl_skip_verify = false + # bind_dn = "uid=%s,ou=users,dc=myorg,dc=com" + +## Grafana's SMTP configuration +## NOTE: To enable, grafana.ini must be configured with smtp.enabled +## ref: http://docs.grafana.org/installation/configuration/#smtp +smtp: + # `existingSecret` is a reference to an existing secret containing the smtp configuration + # for Grafana. + existingSecret: "" + userKey: "user" + passwordKey: "password" + +## Sidecars that collect the configmaps with specified label and stores the included files them into the respective folders +## Requires at least Grafana 5 to work and can't be used together with parameters dashboardProviders, datasources and dashboards +sidecar: + image: + repository: quay.io/kiwigrid/k8s-sidecar + tag: 1.24.3 + sha: "" + imagePullPolicy: IfNotPresent + resources: {} +# limits: +# cpu: 100m +# memory: 100Mi +# requests: +# cpu: 50m +# memory: 50Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + # skipTlsVerify Set to true to skip tls verification for kube api calls + # skipTlsVerify: true + enableUniqueFilenames: false + readinessProbe: {} + livenessProbe: {} + # Log level default for all sidecars. Can be one of: DEBUG, INFO, WARN, ERROR, CRITICAL. Defaults to INFO + # logLevel: INFO + alerts: + enabled: false + # Additional environment variables for the alerts sidecar + env: {} + # Do not reprocess already processed unchanged resources on k8s API reconnect. + # ignoreAlreadyProcessed: true + # label that the configmaps with alert are marked with + label: grafana_alert + # value of label that the configmaps with alert are set to + labelValue: "" + # Log level. Can be one of: DEBUG, INFO, WARN, ERROR, CRITICAL. + # logLevel: INFO + # If specified, the sidecar will search for alert config-maps inside this namespace. + # Otherwise the namespace in which the sidecar is running will be used. + # It's also possible to specify ALL to search in all namespaces + searchNamespace: null + # Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. + watchMethod: WATCH + # search in configmap, secret or both + resource: both + # watchServerTimeout: request to the server, asking it to cleanly close the connection after that. + # defaults to 60sec; much higher values like 3600 seconds (1h) are feasible for non-Azure K8S + # watchServerTimeout: 3600 + # + # watchClientTimeout: is a client-side timeout, configuring your local socket. + # If you have a network outage dropping all packets with no RST/FIN, + # this is how long your client waits before realizing & dropping the connection. + # defaults to 66sec (sic!) + # watchClientTimeout: 60 + # + # Endpoint to send request to reload alerts + reloadURL: "http://localhost:3000/api/admin/provisioning/alerting/reload" + # Absolute path to shell script to execute after a alert got reloaded + script: null + skipReload: false + # Deploy the alert sidecar as an initContainer in addition to a container. + # Sets the size limit of the alert sidecar emptyDir volume + sizeLimit: {} + dashboards: + enabled: false + # Additional environment variables for the dashboards sidecar + env: {} + # Do not reprocess already processed unchanged resources on k8s API reconnect. + # ignoreAlreadyProcessed: true + SCProvider: true + # label that the configmaps with dashboards are marked with + label: grafana_dashboard + # value of label that the configmaps with dashboards are set to + labelValue: "" + # Log level. Can be one of: DEBUG, INFO, WARN, ERROR, CRITICAL. + # logLevel: INFO + # folder in the pod that should hold the collected dashboards (unless `defaultFolderName` is set) + folder: /tmp/dashboards + # The default folder name, it will create a subfolder under the `folder` and put dashboards in there instead + defaultFolderName: null + # Namespaces list. If specified, the sidecar will search for config-maps/secrets inside these namespaces. + # Otherwise the namespace in which the sidecar is running will be used. + # It's also possible to specify ALL to search in all namespaces. + searchNamespace: null + # Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. + watchMethod: WATCH + # search in configmap, secret or both + resource: both + # If specified, the sidecar will look for annotation with this name to create folder and put graph here. + # You can use this parameter together with `provider.foldersFromFilesStructure`to annotate configmaps and create folder structure. + folderAnnotation: null + # Endpoint to send request to reload alerts + reloadURL: "http://localhost:3000/api/admin/provisioning/dashboards/reload" + # Absolute path to shell script to execute after a configmap got reloaded + script: null + skipReload: false + # watchServerTimeout: request to the server, asking it to cleanly close the connection after that. + # defaults to 60sec; much higher values like 3600 seconds (1h) are feasible for non-Azure K8S + # watchServerTimeout: 3600 + # + # watchClientTimeout: is a client-side timeout, configuring your local socket. + # If you have a network outage dropping all packets with no RST/FIN, + # this is how long your client waits before realizing & dropping the connection. + # defaults to 66sec (sic!) + # watchClientTimeout: 60 + # + # provider configuration that lets grafana manage the dashboards + provider: + # name of the provider, should be unique + name: sidecarProvider + # orgid as configured in grafana + orgid: 1 + # folder in which the dashboards should be imported in grafana + folder: '' + # type of the provider + type: file + # disableDelete to activate a import-only behaviour + disableDelete: false + # allow updating provisioned dashboards from the UI + allowUiUpdates: false + # allow Grafana to replicate dashboard structure from filesystem + foldersFromFilesStructure: false + # Additional dashboard sidecar volume mounts + extraMounts: [] + # Sets the size limit of the dashboard sidecar emptyDir volume + sizeLimit: {} + datasources: + enabled: false + # Additional environment variables for the datasourcessidecar + env: {} + # Do not reprocess already processed unchanged resources on k8s API reconnect. + # ignoreAlreadyProcessed: true + # label that the configmaps with datasources are marked with + label: grafana_datasource + # value of label that the configmaps with datasources are set to + labelValue: "" + # Log level. Can be one of: DEBUG, INFO, WARN, ERROR, CRITICAL. + # logLevel: INFO + # If specified, the sidecar will search for datasource config-maps inside this namespace. + # Otherwise the namespace in which the sidecar is running will be used. + # It's also possible to specify ALL to search in all namespaces + searchNamespace: null + # Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. + watchMethod: WATCH + # search in configmap, secret or both + resource: both + # watchServerTimeout: request to the server, asking it to cleanly close the connection after that. + # defaults to 60sec; much higher values like 3600 seconds (1h) are feasible for non-Azure K8S + # watchServerTimeout: 3600 + # + # watchClientTimeout: is a client-side timeout, configuring your local socket. + # If you have a network outage dropping all packets with no RST/FIN, + # this is how long your client waits before realizing & dropping the connection. + # defaults to 66sec (sic!) + # watchClientTimeout: 60 + # + # Endpoint to send request to reload datasources + reloadURL: "http://localhost:3000/api/admin/provisioning/datasources/reload" + # Absolute path to shell script to execute after a datasource got reloaded + script: null + skipReload: false + # Deploy the datasource sidecar as an initContainer in addition to a container. + # This is needed if skipReload is true, to load any datasources defined at startup time. + initDatasources: false + # Sets the size limit of the datasource sidecar emptyDir volume + sizeLimit: {} + plugins: + enabled: false + # Additional environment variables for the plugins sidecar + env: {} + # Do not reprocess already processed unchanged resources on k8s API reconnect. + # ignoreAlreadyProcessed: true + # label that the configmaps with plugins are marked with + label: grafana_plugin + # value of label that the configmaps with plugins are set to + labelValue: "" + # Log level. Can be one of: DEBUG, INFO, WARN, ERROR, CRITICAL. + # logLevel: INFO + # If specified, the sidecar will search for plugin config-maps inside this namespace. + # Otherwise the namespace in which the sidecar is running will be used. + # It's also possible to specify ALL to search in all namespaces + searchNamespace: null + # Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. + watchMethod: WATCH + # search in configmap, secret or both + resource: both + # watchServerTimeout: request to the server, asking it to cleanly close the connection after that. + # defaults to 60sec; much higher values like 3600 seconds (1h) are feasible for non-Azure K8S + # watchServerTimeout: 3600 + # + # watchClientTimeout: is a client-side timeout, configuring your local socket. + # If you have a network outage dropping all packets with no RST/FIN, + # this is how long your client waits before realizing & dropping the connection. + # defaults to 66sec (sic!) + # watchClientTimeout: 60 + # + # Endpoint to send request to reload plugins + reloadURL: "http://localhost:3000/api/admin/provisioning/plugins/reload" + # Absolute path to shell script to execute after a plugin got reloaded + script: null + skipReload: false + # Deploy the datasource sidecar as an initContainer in addition to a container. + # This is needed if skipReload is true, to load any plugins defined at startup time. + initPlugins: false + # Sets the size limit of the plugin sidecar emptyDir volume + sizeLimit: {} + notifiers: + enabled: false + # Additional environment variables for the notifierssidecar + env: {} + # Do not reprocess already processed unchanged resources on k8s API reconnect. + # ignoreAlreadyProcessed: true + # label that the configmaps with notifiers are marked with + label: grafana_notifier + # value of label that the configmaps with notifiers are set to + labelValue: "" + # Log level. Can be one of: DEBUG, INFO, WARN, ERROR, CRITICAL. + # logLevel: INFO + # If specified, the sidecar will search for notifier config-maps inside this namespace. + # Otherwise the namespace in which the sidecar is running will be used. + # It's also possible to specify ALL to search in all namespaces + searchNamespace: null + # Method to use to detect ConfigMap changes. With WATCH the sidecar will do a WATCH requests, with SLEEP it will list all ConfigMaps, then sleep for 60 seconds. + watchMethod: WATCH + # search in configmap, secret or both + resource: both + # watchServerTimeout: request to the server, asking it to cleanly close the connection after that. + # defaults to 60sec; much higher values like 3600 seconds (1h) are feasible for non-Azure K8S + # watchServerTimeout: 3600 + # + # watchClientTimeout: is a client-side timeout, configuring your local socket. + # If you have a network outage dropping all packets with no RST/FIN, + # this is how long your client waits before realizing & dropping the connection. + # defaults to 66sec (sic!) + # watchClientTimeout: 60 + # + # Endpoint to send request to reload notifiers + reloadURL: "http://localhost:3000/api/admin/provisioning/notifications/reload" + # Absolute path to shell script to execute after a notifier got reloaded + script: null + skipReload: false + # Deploy the notifier sidecar as an initContainer in addition to a container. + # This is needed if skipReload is true, to load any notifiers defined at startup time. + initNotifiers: false + # Sets the size limit of the notifier sidecar emptyDir volume + sizeLimit: {} + +## Override the deployment namespace +## +namespaceOverride: "" + +## Number of old ReplicaSets to retain +## +revisionHistoryLimit: 10 + +## Add a seperate remote image renderer deployment/service +imageRenderer: + deploymentStrategy: {} + # Enable the image-renderer deployment & service + enabled: false + replicas: 1 + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPU: "60" + targetMemory: "" + behavior: {} + image: + # image-renderer Image repository + repository: docker.io/grafana/grafana-image-renderer + # image-renderer Image tag + tag: latest + # image-renderer Image sha (optional) + sha: "" + # image-renderer ImagePullPolicy + pullPolicy: Always + # extra environment variables + env: + HTTP_HOST: "0.0.0.0" + # RENDERING_ARGS: --no-sandbox,--disable-gpu,--window-size=1280x758 + # RENDERING_MODE: clustered + # IGNORE_HTTPS_ERRORS: true + + ## "valueFrom" environment variable references that will be added to deployment pods. Name is templated. + ## ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#envvarsource-v1-core + ## Renders in container spec as: + ## env: + ## ... + ## - name: + ## valueFrom: + ## + envValueFrom: {} + # ENV_NAME: + # configMapKeyRef: + # name: configmap-name + # key: value_key + + # image-renderer deployment serviceAccount + serviceAccountName: "" + # image-renderer deployment securityContext + securityContext: {} + # image-renderer deployment container securityContext + containerSecurityContext: + seccompProfile: + type: RuntimeDefault + capabilities: + drop: ['ALL'] + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + # image-renderer deployment Host Aliases + hostAliases: [] + # image-renderer deployment priority class + priorityClassName: '' + service: + # Enable the image-renderer service + enabled: true + # image-renderer service port name + portName: 'http' + # image-renderer service port used by both service and deployment + port: 8081 + targetPort: 8081 + # Adds the appProtocol field to the image-renderer service. This allows to work with istio protocol selection. Ex: "http" or "tcp" + appProtocol: "" + serviceMonitor: + ## If true, a ServiceMonitor CRD is created for a prometheus operator + ## https://github.com/coreos/prometheus-operator + ## + enabled: false + path: /metrics + # namespace: monitoring (defaults to use the namespace this chart is deployed to) + labels: {} + interval: 1m + scheme: http + tlsConfig: {} + scrapeTimeout: 30s + relabelings: [] + # See: https://doc.crds.dev/github.com/prometheus-operator/kube-prometheus/monitoring.coreos.com/ServiceMonitor/v1@v0.11.0#spec-targetLabels + targetLabels: [] + # - targetLabel1 + # - targetLabel2 + # If https is enabled in Grafana, this needs to be set as 'https' to correctly configure the callback used in Grafana + grafanaProtocol: http + # In case a sub_path is used this needs to be added to the image renderer callback + grafanaSubPath: "" + # name of the image-renderer port on the pod + podPortName: http + # number of image-renderer replica sets to keep + revisionHistoryLimit: 10 + networkPolicy: + # Enable a NetworkPolicy to limit inbound traffic to only the created grafana pods + limitIngress: true + # Enable a NetworkPolicy to limit outbound traffic to only the created grafana pods + limitEgress: false + # Allow additional services to access image-renderer (eg. Prometheus operator when ServiceMonitor is enabled) + extraIngressSelectors: [] + resources: {} +# limits: +# cpu: 100m +# memory: 100Mi +# requests: +# cpu: 50m +# memory: 50Mi + ## Node labels for pod assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + # + nodeSelector: {} + + ## Tolerations for pod assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [] + + ## Affinity for pod assignment (evaluated as template) + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## + affinity: {} + + ## Use an alternate scheduler, e.g. "stork". + ## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ + ## + # schedulerName: "default-scheduler" + +networkPolicy: + ## @param networkPolicy.enabled Enable creation of NetworkPolicy resources. Only Ingress traffic is filtered for now. + ## + enabled: false + ## @param networkPolicy.allowExternal Don't require client label for connections + ## The Policy model to apply. When set to false, only pods with the correct + ## client label will have network access to grafana port defined. + ## When true, grafana will accept connections from any source + ## (with the correct destination port). + ## + ingress: true + ## @param networkPolicy.ingress When true enables the creation + ## an ingress network policy + ## + allowExternal: true + ## @param networkPolicy.explicitNamespacesSelector A Kubernetes LabelSelector to explicitly select namespaces from which traffic could be allowed + ## If explicitNamespacesSelector is missing or set to {}, only client Pods that are in the networkPolicy's namespace + ## and that match other criteria, the ones that have the good label, can reach the grafana. + ## But sometimes, we want the grafana to be accessible to clients from other namespaces, in this case, we can use this + ## LabelSelector to select these namespaces, note that the networkPolicy's namespace should also be explicitly added. + ## + ## Example: + ## explicitNamespacesSelector: + ## matchLabels: + ## role: frontend + ## matchExpressions: + ## - {key: role, operator: In, values: [frontend]} + ## + explicitNamespacesSelector: {} + ## + ## + ## + ## + ## + ## + egress: + ## @param networkPolicy.egress.enabled When enabled, an egress network policy will be + ## created allowing grafana to connect to external data sources from kubernetes cluster. + enabled: false + ## + ## @param networkPolicy.egress.ports Add individual ports to be allowed by the egress + ports: [] + ## Add ports to the egress by specifying - port: + ## E.X. + ## ports: + ## - port: 80 + ## - port: 443 + ## + ## + ## + ## + ## + ## + +# Enable backward compatibility of kubernetes where version below 1.13 doesn't have the enableServiceLinks option +enableKubeBackwardCompatibility: false +useStatefulSet: false +# Create a dynamic manifests via values: +extraObjects: [] + # - apiVersion: "kubernetes-client.io/v1" + # kind: ExternalSecret + # metadata: + # name: grafana-secrets + # spec: + # backendType: gcpSecretsManager + # data: + # - key: grafana-admin-password + # name: adminPassword diff --git a/charts/devlake/templates/_helpers.tpl b/charts/devlake/templates/_helpers.tpl index 25e8037..3f5911e 100644 --- a/charts/devlake/templates/_helpers.tpl +++ b/charts/devlake/templates/_helpers.tpl @@ -170,7 +170,7 @@ The database url */}} {{- define "database.url" -}} {{- if eq .Values.option.database "mysql" -}} -mysql://{{ .Values.mysql.username }}:{{ .Values.mysql.password }}@{{ include "mysql.server" . }}:{{ include "mysql.port" . }}/{{ .Values.mysql.database }}?charset=utf8mb4&parseTime=True&loc={{ .Values.envs.TZ }} +mysql://{{ .Values.mysql.username }}:{{ .Values.mysql.password }}@{{ include "mysql.server" . }}:{{ include "mysql.port" . }}/{{ .Values.mysql.database }}?charset=utf8mb4&parseTime=True&loc={{ .Values.commonEnvs.TZ }} {{- end }} {{- end }} diff --git a/charts/devlake/templates/deployments.yaml b/charts/devlake/templates/deployments.yaml index 37ca9ce..02390e8 100644 --- a/charts/devlake/templates/deployments.yaml +++ b/charts/devlake/templates/deployments.yaml @@ -67,7 +67,7 @@ spec: - name: GRAFANA_ENDPOINT value: {{ .Release.Name }}-grafana.{{ .Release.Namespace }}.svc.cluster.local:80 {{- end }} - {{- range $key, $value := .Values.envs }} + {{- range $key, $value := .Values.commonEnvs }} - name: "{{ tpl $key $ }}" value: "{{ tpl (print $value) $ }}" {{- end }} @@ -146,15 +146,20 @@ spec: name: {{ include "devlake.mysql.secret" . }} - secretRef: name: {{ include "devlake.lake.encryption.secret" . }} + {{- if .Values.lake.extraEnvsFromSecret }} + - secretRef: + name: {{ .Values.lake.extraEnvsFromSecret }} + {{- end }} env: - name: PORT value: "{{ .Values.lake.port }}" - {{- with .Values.lake.envs }} - {{- toYaml . | nindent 12 }} + {{- range $key1, $value1 := .Values.lake.envs }} + - name: "{{ tpl $key1 $ }}" + value: "{{ tpl (print $value1) $ }}" {{- end }} - {{- range $key, $value := .Values.envs }} - - name: "{{ tpl $key $ }}" - value: "{{ tpl (print $value) $ }}" + {{- range $key2, $value2 := .Values.commonEnvs }} + - name: "{{ tpl $key2 $ }}" + value: "{{ tpl (print $value2) $ }}" {{- end }} {{- with .Values.lake.resources }} resources: diff --git a/charts/devlake/templates/statefulsets.yaml b/charts/devlake/templates/statefulsets.yaml index e3bfb47..c022f3a 100644 --- a/charts/devlake/templates/statefulsets.yaml +++ b/charts/devlake/templates/statefulsets.yaml @@ -80,7 +80,7 @@ spec: - secretRef: name: {{ include "devlake.mysql.secret" . }} env: - {{- range $key, $value := .Values.envs }} + {{- range $key, $value := .Values.commonEnvs }} - name: "{{ tpl $key $ }}" value: "{{ tpl (print $value) $ }}" {{- end }} diff --git a/charts/devlake/templates/validate.yaml b/charts/devlake/templates/validate.yaml index bedc9b0..c74296d 100644 --- a/charts/devlake/templates/validate.yaml +++ b/charts/devlake/templates/validate.yaml @@ -1,6 +1,6 @@ {{- if and .Values.lake.encryptionSecret.autoCreateSecret (not .Values.lake.encryptionSecret.secret) }} {{- fail -"Helm test requires lake.encryptionSecret.secret.\n\n - If you're upgrading from DevLake v0.17.x or earlier versions, please get the encryption secret by copying the ENCODE_KEY value from /app/config/.env of the lake pod (e.g. devlake-lake-0);\n - If not, get the encryption secret via command `openssl rand -base64 2000 | tr -dc 'A-Z' | fold -w 128 | head -n 1`.\n\nFor more information, please check https://github.com/apache/incubator-devlake-helm-chart" +"Helm test requires lake.encryptionSecret.secret.\n\n - If you're upgrading from DevLake v0.17.x or earlier versions, please get the encryption secret by copying the ENCODE_KEY value from /app/config/.env of the lake pod (e.g. devlake-lake-0);\n - If upgrading from v0.18.0+, get the original secret in k8s secret and decode it\n - If new installation, get the encryption secret via command `openssl rand -base64 2000 | tr -dc 'A-Z' | fold -w 128 | head -n 1`.\n\nFor more information, please check https://github.com/apache/incubator-devlake-helm-chart" }} {{- end }} diff --git a/charts/devlake/values.yaml b/charts/devlake/values.yaml index b3276dd..4b75323 100644 --- a/charts/devlake/values.yaml +++ b/charts/devlake/values.yaml @@ -19,8 +19,8 @@ replicaCount: 1 imageTag: v0.19.0-beta6 -#the common environments for all pods -envs: +#the common environments for all pods except grafana, grafana needs to be set in grafana section seperately +commonEnvs: TZ: "UTC" mysql: @@ -141,16 +141,18 @@ grafana: image: repository: devlake.docker.scarf.sh/apache/devlake-dashboard tag: v0.19.0-beta6 + adminPassword: "" grafana.ini: server: root_url: "%(protocol)s://%(domain)s/grafana" #the secret name should be as same as .Values.option.connectionSecretName envFromSecrets: - name: "devlake-mysql-auth" - #keep grafana timezone same as other pods, which is set by .Values.envs.TZ + #keep grafana timezone same as other pods, which is set by .Values.commonEnvs.TZ env: TZ: "UTC" + lake: image: repository: devlake.docker.scarf.sh/apache/devlake @@ -160,22 +162,16 @@ lake: # storage for config port: 8080 envs: - - name: API_TIMEOUT - value: "120s" - - name: API_RETRY - value: "3" - - name: API_REQUESTS_PER_HOUR - value: "10000" - - name: PIPELINE_MAX_PARALLEL - value: "1" - - name: IN_SECURE_SKIP_VERIFY - value: "false" - - name: LOGGING_DIR - value: "/app/logs" + API_TIMEOUT: "120s" + API_RETRY: "3" + API_REQUESTS_PER_HOUR: "10000" + PIPELINE_MAX_PARALLEL: "1" + IN_SECURE_SKIP_VERIFY: "false" + LOGGING_DIR: "/app/logs" # debug, info, warn, error - - name: LOGGING_LEVEL - value: "info" - + LOGGING_LEVEL: "info" + #extra envs from an existing secret + extraEnvsFromSecret: "" encryptionSecret: # The name of secret which contains keys named ENCRYPTION_SECRET secretName: ""