From f71efa6e6f2b181931d1a55faf23eed60f2daeec Mon Sep 17 00:00:00 2001 From: Stepan Kazakov Date: Wed, 15 Nov 2023 22:52:01 +0300 Subject: [PATCH 1/3] chart: add static replicas count --- deploy/k8s/chart/Chart.yaml | 2 ++ deploy/k8s/chart/README.md | 7 ++++++- deploy/k8s/chart/templates/deployment.yaml | 3 +++ deploy/k8s/chart/values.yaml | 5 ++++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/deploy/k8s/chart/Chart.yaml b/deploy/k8s/chart/Chart.yaml index 42ad8b5..771b52c 100644 --- a/deploy/k8s/chart/Chart.yaml +++ b/deploy/k8s/chart/Chart.yaml @@ -4,3 +4,5 @@ name: cortex-tenant version: 0.3.2 # This is the chart version appVersion: 1.11.0 # version number of the application being deployed. type: application +sources: + - https://github.com/blind-oracle/cortex-tenant diff --git a/deploy/k8s/chart/README.md b/deploy/k8s/chart/README.md index 872c3ce..30a6f65 100644 --- a/deploy/k8s/chart/README.md +++ b/deploy/k8s/chart/README.md @@ -4,6 +4,10 @@ A Helm Chart for cortex-tenant +## Source Code + +* + ## Values | Key | Type | Default | Description | @@ -48,6 +52,7 @@ A Helm Chart for cortex-tenant | podDisruptionBudget.minAvailable | int | `1` | Minimum number of pods that must remain scheduled | | podSecurityContext | object | `{}` | [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | | podTopologySpreadConstraints | list | `[]` | [Pod Topology Spread Constraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/) | +| replicas | int | `1` | Number of replicas. Ignored if `autoscaling.enabled` is true | | resources.limits | object | `{"memory":"256Mi"}` | Resources limits | | resources.requests | object | `{"cpu":"100m","memory":"128Mi"}` | Resources requests | | securityContext | object | `{}` | [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | @@ -71,4 +76,4 @@ A Helm Chart for cortex-tenant | tolerations | list | `[]` | [Taints and Tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.2](https://github.com/norwoodj/helm-docs/releases/v1.11.2) +Autogenerated from chart metadata using [helm-docs v1.11.3](https://github.com/norwoodj/helm-docs/releases/v1.11.3) diff --git a/deploy/k8s/chart/templates/deployment.yaml b/deploy/k8s/chart/templates/deployment.yaml index dcf24cf..aeec939 100644 --- a/deploy/k8s/chart/templates/deployment.yaml +++ b/deploy/k8s/chart/templates/deployment.yaml @@ -9,6 +9,9 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicas }} + {{- end }} selector: matchLabels: {{- include "cortex-tenant.selectorLabels" . | nindent 6 }} diff --git a/deploy/k8s/chart/values.yaml b/deploy/k8s/chart/values.yaml index e6d61b4..3a9326a 100644 --- a/deploy/k8s/chart/values.yaml +++ b/deploy/k8s/chart/values.yaml @@ -19,6 +19,9 @@ service: # The target port to which traffic is forwarded targetPort: 8080 +# -- Number of replicas. Ignored if `autoscaling.enabled` is true +replicas: 1 + autoscaling: # -- If enabled, HorizontalPodAutoscaler resources are created enabled: true @@ -186,7 +189,7 @@ serviceMonitor: # https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/api.md#relabelconfig # (defines `metric_relabel_configs`) metricRelabelings: [] - # --ServiceMonitor will add labels from the service to the Prometheus metric + # -- ServiceMonitor will add labels from the service to the Prometheus metric # https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#servicemonitorspec targetLabels: [] # -- ServiceMonitor will use http by default, but you can pick https as well From ca3dfe6c550551438e508e296e5ba9c1b52c642d Mon Sep 17 00:00:00 2001 From: Stepan Kazakov Date: Wed, 15 Nov 2023 23:03:11 +0300 Subject: [PATCH 2/3] chart: add json schema validation --- .gitignore | 5 +- deploy/k8s/chart/values.schema.json | 523 ++++++++++++++++++++++++++++ 2 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 deploy/k8s/chart/values.schema.json diff --git a/.gitignore b/.gitignore index 460c20f..2660880 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ generate .out *.rpm *.deb -config.yaml \ No newline at end of file +config.yaml +.vscode +.idea +.DS_Store diff --git a/deploy/k8s/chart/values.schema.json b/deploy/k8s/chart/values.schema.json new file mode 100644 index 0000000..9b1458d --- /dev/null +++ b/deploy/k8s/chart/values.schema.json @@ -0,0 +1,523 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "A helm chart for cortex-tenant", + "description": "Prometheus remote write proxy which marks timeseries with a Cortex/Mimir tenant ID based on labels.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "nameOverride": { + "type": [ + "string", + "null" + ], + "title": "Name override", + "description": "Overrides the chart name" + }, + "fullnameOverride": { + "type": [ + "string", + "null" + ], + "title": "Fullname override", + "description": "Overrides the fullname of the chart" + }, + "image": { + "type": "object", + "title": "Image", + "properties": { + "repository": { + "type": "string", + "title": "Repository", + "description": "Image repository" + }, + "pullPolicy": { + "type": "string", + "title": "Pull policy", + "description": "Image pull policy", + "enum": [ + "Always", + "Never", + "IfNotPresent" + ] + }, + "tag": { + "type": "string", + "title": "Tag", + "description": "Image tag" + } + } + }, + "service": { + "type": "object", + "title": "Service", + "properties": { + "type": { + "type": "string", + "title": "Type", + "description": "Service type", + "enum": [ + "ClusterIP", + "NodePort", + "LoadBalancer" + ] + }, + "port": { + "type": "integer", + "title": "Port", + "description": "Service port", + "default": 8080 + }, + "targetPort": { + "type": "integer", + "title": "Target port", + "description": "Service target port", + "default": 8080 + } + } + }, + "replicas": { + "type": "integer", + "title": "Replicas", + "description": "Number of replicas", + "default": 1 + }, + "autoscaling": { + "type": "object", + "title": "Autoscaling", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enable autoscaling" + }, + "minReplica": { + "type": "integer", + "title": "Min replicas", + "description": "Minimum number of replicas", + "default": 1 + }, + "maxReplicas": { + "type": "integer", + "title": "Max replicas", + "description": "Maximum number of replicas", + "default": 2 + }, + "targetCPUUtilizationPercentage": { + "type": "integer", + "title": "Target CPU utilization percentage", + "description": "Target CPU utilization percentage", + "default": 50 + }, + "targetMemoryAverageValue": { + "type": "string", + "title": "Target memory average value", + "description": "Target memory average value", + "default": "100Mi" + } + } + }, + "envs": { + "type": "array", + "title": "Envs", + "description": "Environment variables", + "items": { + "type": "object", + "title": "Env", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Environment variable name" + }, + "value": { + "type": [ + "string", + "integer", + "boolean" + ], + "title": "Value", + "description": "Environment variable value" + } + } + } + }, + "config": { + "type": "object", + "title": "Config", + "description": "Configuration for the cortex-tenant", + "properties": { + "listen": { + "type": "string", + "title": "Listen", + "description": "Where to listen for incoming write requests from Prometheus" + }, + "listen_pprof": { + "type": "string", + "title": "Listen Profiling API", + "description": "Profiling API, leave empty to disable" + }, + "target": { + "type": "string", + "title": "Target URL", + "description": "Where to send the modified requests (Cortex)", + "format": "uri" + }, + "enable_ipv6": { + "type": "boolean", + "title": "Enable IPv6", + "description": "Whether to enable querying for IPv6 records" + }, + "log_level": { + "type": "string", + "title": "Log level", + "enum": [ + "debug", + "info", + "warn", + "error" + ], + "default": "warn" + }, + "timeout": { + "type": "string", + "title": "Timeout", + "description": "HTTP request timeout", + "format": "duration" + }, + "timeout_shutdown": { + "type": "string", + "title": "Shutdown timeout", + "description": "Timeout to wait on shutdown to allow load balancers detect that we're going away", + "format": "duration" + }, + "concurrency": { + "type": "integer", + "title": "Concurrency", + "description": "Max number of parallel incoming HTTP requests to handle", + "default": 1000 + }, + "metadata": { + "type": "boolean", + "title": "Metadata", + "description": "Whether to forward metrics metadata from Prometheus to Cortex" + }, + "log_response_errors": { + "type": "boolean", + "title": "Log response errors", + "description": "If true response codes from metrics backend will be logged to stdout" + }, + "max_connection_duration": { + "type": "string", + "title": "Max Connection Duration", + "description": "Maximum duration to keep outgoing connections alive (to Cortex/Mimir)", + "format": "duration" + }, + "auth": { + "type": "object", + "title": "Authentication", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Egress HTTP basic auth -> add `Authentication` header to outgoing requests", + "default": false + }, + "username": { + "type": [ + "string", + "null" + ], + "title": "Username", + "description": "Username" + }, + "password": { + "type": [ + "string", + "null" + ], + "title": "Password", + "description": "Password" + }, + "existingSecret": { + "type": [ + "string", + "null" + ], + "title": "Existing Secret", + "description": "Secret should pass the `CT_AUTH_EGRESS_USERNAME` and `CT_AUTH_EGRESS_PASSWORD` env variables" + } + } + }, + "tenant": { + "type": "object", + "title": "Tenant configuration", + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "Which label to look for the tenant information", + "default": "tenant" + }, + "prefix": { + "type": "string", + "title": "Prefix", + "description": "Optional hard-coded prefix with delimeter for all tenant values" + }, + "label_remove": { + "type": "boolean", + "title": "Label Remove", + "description": "Whether to remove the tenant label from the request", + "default": false + }, + "header": { + "type": "string", + "title": "Header", + "description": "To which header to add the tenant ID", + "default": "X-Scope-OrgID" + }, + "default": { + "type": "string", + "title": "Default", + "description": "Which tenant ID to use if the label is missing in any of the timeseries", + "default": "cortex-tenant-default" + }, + "accept_all": { + "type": "boolean", + "title": "Accept All", + "description": "Enable if you want all metrics from Prometheus to be accepted with a 204 HTTP code", + "default": false + } + } + } + } + }, + "resources": { + "type": "object", + "title": "Resources", + "description": "Resource limits and requests", + "properties": { + "limits": { + "type": "object", + "title": "Limits", + "description": "Resource limits", + "properties": { + "memory": { + "type": [ + "string", + "integer", + "null" + ], + "title": "Memory", + "description": "Maximum memory limit" + }, + "cpu": { + "type": [ + "string", + "integer", + "null" + ], + "title": "CPU", + "description": "Maximum CPU limit" + } + } + }, + "requests": { + "type": "object", + "title": "Requests", + "description": "Resource requests", + "properties": { + "cpu": { + "type": [ + "string", + "integer", + "null" + ], + "title": "CPU", + "description": "CPU request" + }, + "memory": { + "type": [ + "string", + "integer", + "null" + ], + "title": "Memory", + "description": "Memory request" + } + } + } + } + }, + "podDisruptionBudget": { + "type": "object", + "title": "Pod Disruption Budget", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether pod disruption budget is enabled" + }, + "minAvailable": { + "type": [ + "integer", + "string" + ], + "title": "Minimum Available", + "description": "Minimum number of available pods during disruption. Also may be a percentage" + }, + "maxUnavailable": { + "type": [ + "integer", + "string" + ], + "title": "Maximum Unavailable", + "description": "Maximum number of unavailable pods during disruption. Also may be a percentage" + } + } + }, + "annotations": { + "type": "object", + "title": "Annotations", + "description": "Annotations for the deployment" + }, + "podAnnotations": { + "type": "object", + "title": "Pod Annotations", + "description": "Annotations for the pods" + }, + "podSecurityContext": { + "type": "object", + "title": "Pod Security Context", + "description": "Security context for the pods" + }, + "securityContext": { + "type": "object", + "title": "Security Context", + "description": "Security context" + }, + "nodeSelector": { + "type": "object", + "title": "Node Selector" + }, + "tolerations": { + "type": "array", + "title": "Tolerations" + }, + "affinity": { + "type": "object", + "title": "Affinity" + }, + "podTopologySpreadConstraints": { + "type": "array", + "title": "Pod Topology Spread Constraints" + }, + "serviceMonitor": { + "type": "object", + "title": "Service Monitor", + "description": "Service monitor", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether service monitor is enabled", + "default": false + }, + "namespace": { + "type": [ + "string", + "null" + ], + "title": "Namespace", + "description": "Alternative namespace for ServiceMonitor resources" + }, + "targetPort": { + "type": "integer", + "title": "Target Port", + "description": "Service targetPort", + "default": 9090 + }, + "namespaceSelector": { + "type": "object", + "title": "Namespace Selector", + "description": "Namespace selector for ServiceMonitor resources" + }, + "annotations": { + "type": "object", + "title": "Annotations", + "description": "Annotations for the service monitor" + }, + "labels": { + "type": "object", + "title": "Labels", + "description": "Additional ServiceMonitor labels" + }, + "interval": { + "type": [ + "string", + "null" + ], + "title": "Interval", + "description": "ServiceMonitor scrape interval", + "format": "duration" + }, + "scrapeTimeout": { + "type": [ + "string", + "null" + ], + "title": "Scrape Timeout", + "description": "ServiceMonitor scrape timeout in Go duration format (e.g. 15s)", + "format": "duration" + }, + "relabelings": { + "type": "array", + "title": "Relabelings", + "description": "ServiceMonitor relabel configs to apply to samples before scraping" + }, + "metricRelabelings": { + "type": "array", + "title": "Metric Relabelings", + "description": "ServiceMonitor relabel configs to apply to samples as the last" + }, + "targetLabels": { + "type": "array", + "title": "Target Labels", + "description": "ServiceMonitor will add labels from the service to the Prometheus metric" + }, + "scheme": { + "type": "string", + "title": "Scheme", + "description": "ServiceMonitor will use http by default, but you can pick https as well", + "default": "http" + }, + "tlsConfig": { + "type": [ + "object", + "null" + ], + "title": "TLS Config" + }, + "prometheusRule": { + "type": "object", + "title": "Prometheus Rule", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled" + }, + "additionalLabels": { + "type": "object", + "title": "Additional Labels" + }, + "rules": { + "type": "array", + "title": "Rules" + } + } + } + } + } + } +} From 0616c56eae4fc08dbe7dbdeb7f2e38cba98b4735 Mon Sep 17 00:00:00 2001 From: Stepan Kazakov Date: Wed, 15 Nov 2023 23:30:17 +0300 Subject: [PATCH 3/3] bump chart version to 0.4.0 --- deploy/k8s/chart/Chart.yaml | 2 +- deploy/k8s/chart/README.md | 2 +- docs/cortex-tenant-0.4.0.tgz | Bin 0 -> 9245 bytes docs/index.yaml | 16 ++++++++++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 docs/cortex-tenant-0.4.0.tgz diff --git a/deploy/k8s/chart/Chart.yaml b/deploy/k8s/chart/Chart.yaml index 771b52c..38ac7e4 100644 --- a/deploy/k8s/chart/Chart.yaml +++ b/deploy/k8s/chart/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 description: A Helm Chart for cortex-tenant name: cortex-tenant -version: 0.3.2 # This is the chart version +version: 0.4.0 # This is the chart version appVersion: 1.11.0 # version number of the application being deployed. type: application sources: diff --git a/deploy/k8s/chart/README.md b/deploy/k8s/chart/README.md index 30a6f65..6bcdb9d 100644 --- a/deploy/k8s/chart/README.md +++ b/deploy/k8s/chart/README.md @@ -1,6 +1,6 @@ # cortex-tenant -![Version: 0.3.2](https://img.shields.io/badge/Version-0.3.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.11.0](https://img.shields.io/badge/AppVersion-1.11.0-informational?style=flat-square) +![Version: 0.4.0](https://img.shields.io/badge/Version-0.4.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.11.0](https://img.shields.io/badge/AppVersion-1.11.0-informational?style=flat-square) A Helm Chart for cortex-tenant diff --git a/docs/cortex-tenant-0.4.0.tgz b/docs/cortex-tenant-0.4.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1ff85ad782cd1631e125511a825560f4da169444 GIT binary patch literal 9245 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