From 584d372521a9a639b12a5093a2f62d6b4c0f8a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20B=C3=A4hler?= Date: Thu, 4 Dec 2025 12:18:07 +0100 Subject: [PATCH] feat(config): add combined users property as successor for usergroups (#1767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(config): add combined users property as successor for usergroups and usernames configuration Signed-off-by: Oliver Bähler * fix(crds): add proper deprecation notices on properties and via admission warnings Signed-off-by: Oliver Bähler * chore: add local monitoring environment Signed-off-by: Oliver Bähler --------- Signed-off-by: Oliver Bähler --- .github/workflows/e2e-internal.yml | 49 - .github/workflows/e2e.yml | 33 +- .github/workflows/lint.yml | 1 + DEVELOPMENT.md | 6 +- Makefile | 22 +- api/v1beta2/capsuleconfiguration_types.go | 7 + api/v1beta2/namespace_options.go | 3 +- api/v1beta2/tenant_types.go | 6 +- api/v1beta2/zz_generated.deepcopy.go | 5 + charts/capsule/README.md | 12 +- .../ci/{tracing-values.yaml => tracing.yaml} | 0 ...sule.clastix.io_capsuleconfigurations.yaml | 32 +- .../crds/capsule.clastix.io_tenants.yaml | 74 +- charts/capsule/templates/configuration.yaml | 4 + .../capsule/templates/crd-lifecycle/job.yaml | 2 +- .../validatingwebhookconfiguration.yaml | 36 + charts/capsule/values.schema.json | 57 +- charts/capsule/values.yaml | 32 +- cmd/main.go | 19 +- e2e/custom_capsule_group_test.go | 25 +- hack/distro/fluxcd/kustomization.yaml | 33 + hack/distro/host-proxy/deploy.yaml | 43 + hack/distro/host-proxy/kustomization.yaml | 5 + hack/distro/host-proxy/servicemonitor.yaml | 33 + .../dashboards/ctrl-runtime-admission.json | 515 ++ .../monitoring/dashboards/ctrl-runtime.json | 546 ++ .../monitoring/dashboards/kustomization.yaml | 22 + .../monitoring/dashboards/performance.json | 4466 +++++++++++++++++ .../kube-prometheus-stack/kustomization.yaml | 4 + .../kube-prometheus-stack/release.flux.yaml | 100 + hack/distro/monitoring/kustomization.yaml | 6 + hack/distro/monitoring/ns.yaml | 4 + .../monitoring/pyroscope/kustomization.yaml | 4 + .../monitoring/pyroscope/release.flux.yaml | 57 + internal/controllers/tenant/limitranges.go | 4 +- internal/controllers/tenant/manager.go | 5 +- .../controllers/tenant/networkpolicies.go | 4 +- internal/webhook/cfg/warnings.go | 71 + internal/webhook/ingress/errors.go | 1 + .../webhook/ingress/validate_hostnames.go | 1 + .../webhook/pod/containerregistry_errors.go | 1 + internal/webhook/route/config.go | 24 + .../validation/containerregistry_regex.go | 1 + .../tenant/validation/hostname_regex.go | 1 + .../tenant/validation/ingressclass_regex.go | 1 + .../tenant/validation/storageclass_regex.go | 1 + .../webhook/tenant/validation/warnings.go | 35 +- internal/webhook/utils/error.go | 1 + pkg/api/allowed_list.go | 4 +- pkg/api/owner_status_list.go | 1 + pkg/api/users_list.go | 69 +- pkg/api/users_list_test.go | 325 ++ pkg/configuration/client.go | 8 +- pkg/utils/tenant/metdata.go | 5 + 54 files changed, 6700 insertions(+), 126 deletions(-) delete mode 100644 .github/workflows/e2e-internal.yml rename charts/capsule/ci/{tracing-values.yaml => tracing.yaml} (100%) create mode 100644 hack/distro/fluxcd/kustomization.yaml create mode 100644 hack/distro/host-proxy/deploy.yaml create mode 100644 hack/distro/host-proxy/kustomization.yaml create mode 100644 hack/distro/host-proxy/servicemonitor.yaml create mode 100644 hack/distro/monitoring/dashboards/ctrl-runtime-admission.json create mode 100644 hack/distro/monitoring/dashboards/ctrl-runtime.json create mode 100644 hack/distro/monitoring/dashboards/kustomization.yaml create mode 100644 hack/distro/monitoring/dashboards/performance.json create mode 100644 hack/distro/monitoring/kube-prometheus-stack/kustomization.yaml create mode 100644 hack/distro/monitoring/kube-prometheus-stack/release.flux.yaml create mode 100644 hack/distro/monitoring/kustomization.yaml create mode 100644 hack/distro/monitoring/ns.yaml create mode 100644 hack/distro/monitoring/pyroscope/kustomization.yaml create mode 100644 hack/distro/monitoring/pyroscope/release.flux.yaml create mode 100644 internal/webhook/cfg/warnings.go create mode 100644 internal/webhook/route/config.go create mode 100644 pkg/api/users_list_test.go diff --git a/.github/workflows/e2e-internal.yml b/.github/workflows/e2e-internal.yml deleted file mode 100644 index 243e3c37..00000000 --- a/.github/workflows/e2e-internal.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: e2e-internal -permissions: {} - -on: - pull_request_target: - types: - - opened - - synchronize - - reopened - branches: - - "main" - paths: - - '.github/workflows/e2e.yml' - - '.github/workflows/e2e-internal.yml' - - 'api/**' - - 'controllers/**' - - 'internal/**' - - 'pkg/**' - - 'e2e/*' - - 'Dockerfile' - - 'go.*' - - 'main.go' - - 'Makefile' - -jobs: - internal-e2e: - name: Trigger internal E2E Testing - runs-on: - labels: ubuntu-latest - if: github.repository_owner == 'projectcapsule' - steps: - - name: Trigger internal e2e repo - env: - GH_TOKEN: ${{ secrets.INTERNAL_E2E_PAT }} - run: | - if [ -z "${GH_TOKEN}" ]; then - echo "GH_TOKEN is empty; secrets are not available. Skipping." - exit 1 - fi - - curl -fsS -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GH_TOKEN" \ - https://api.github.com/repos/projectcapsule/enterprise-e2e/dispatches \ - -d "$(jq -n --arg ref "main" \ - --arg pr_number "${{ github.event.pull_request.number }}" \ - --arg sha "${{ github.sha }}" \ - --arg repo "${{ github.repository }}" \ - '{event_type:"internal-e2e", client_payload:{ref:$ref, pr_number:$pr_number, sha:$sha, repo:$repo}}')" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d20f6984..d0fa49ea 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,18 +23,45 @@ concurrency: jobs: e2e: - name: E2E Testing + name: E2E Testing (CE) runs-on: labels: ubuntu-latest-8-cores steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version-file: 'go.mod' + - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 - with: - version: v3.14.2 + - name: e2e run: sudo make e2e + run-e2e: + name: E2E Testing + strategy: + fail-fast: false + matrix: + k8s-version: + - '1.30.0' + - '1.31.0' + - '1.32.0' + - '1.33.0' + runs-on: + labels: ubuntu-latest-8-cores + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + repository: ${{ github.event.client_payload.repo }} + ref: ${{ github.event.client_payload.sha }} + + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + with: + go-version-file: 'go.mod' + + - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + + - name: e2e (Enterprise) + run: KUBERNETES_SUPPORTED_VERSION=${{ matrix.k8s-version }} sudo make e2e diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 524c2c9c..3b245fe6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,6 +23,7 @@ jobs: go-version-file: 'go.mod' - name: Generate manifests run: | + make generate make manifests if [[ $(git diff --stat) != '' ]]; then echo -e '\033[0;31mManifests outdated! (Run make manifests locally and commit)\033[0m ❌' diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 056fb455..a7ac0969 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -70,9 +70,13 @@ $ make deploy # To retrieve your laptop's IP and execute `make dev-setup` to setup dev env # For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup $ LAPTOP_HOST_IP="" make dev-setup + + +# Monitoring Setup (Grafana/Prometheus/Pyroscope) +$ LAPTOP_HOST_IP="" make dev-setup-monitoring ``` -### Explenation +### Setup We recommend to setup the development environment with the make `dev-setup` target. However here is a step by step guide to setup the development environment for understanding. diff --git a/Makefile b/Makefile index edc1ed6b..af598093 100644 --- a/Makefile +++ b/Makefile @@ -97,9 +97,7 @@ helm-test: kind @$(KIND) delete cluster --name capsule-charts helm-test-exec: ct helm-controller-version ko-build-all - $(MAKE) docker-build-capsule-trace $(MAKE) e2e-load-image CLUSTER_NAME=capsule-charts IMAGE=$(CAPSULE_IMG) VERSION=v0.0.0 - $(MAKE) e2e-load-image CLUSTER_NAME=capsule-charts IMAGE=$(CAPSULE_IMG) VERSION=tracing @$(KUBECTL) create ns capsule-system || true @$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/grafana/grafana-operator/releases/download/v5.18.0/crds.yaml @$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.crds.yaml @@ -160,6 +158,26 @@ dev-setup: ./charts/capsule $(KUBECTL) -n capsule-system scale deployment capsule-controller-manager --replicas=0 || true +setup-monitoring: dev-setup-fluxcd + @$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring | envsubst | kubectl apply -f - + @$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring/dashboards | kubectl apply -f - + @$(MAKE) wait-for-helmreleases + @printf "\n\033[32mAccess Grafana:\033[0m\n\n" + @printf " \033[1mkubectl port-forward svc/kube-prometheus-stack-grafana 9090:80 -n monitoring-system\033[0m\n\n" + +dev-setup-monitoring: setup-monitoring + @$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/host-proxy | envsubst | kubectl apply -f - + +dev-setup-fluxcd: + @$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/fluxcd | envsubst | kubectl apply -f - + +wait-for-helmreleases: + @ echo "Waiting for all HelmReleases to have observedGeneration >= 0..." + @while [ "$$($(KUBECTL) get helmrelease -A -o jsonpath='{range .items[?(@.status.observedGeneration<0)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' | wc -l)" -ne 0 ]; do \ + sleep 5; \ + done + + #################### # -- Docker #################### diff --git a/api/v1beta2/capsuleconfiguration_types.go b/api/v1beta2/capsuleconfiguration_types.go index 69b10865..894f0fb1 100644 --- a/api/v1beta2/capsuleconfiguration_types.go +++ b/api/v1beta2/capsuleconfiguration_types.go @@ -11,8 +11,15 @@ import ( // CapsuleConfigurationSpec defines the Capsule configuration. type CapsuleConfigurationSpec struct { + // Define entities which are considered part of the Capsule construct + // Users not mentioned here will be ignored by Capsule + Users api.UserListSpec `json:"users,omitempty"` + // Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users) + // // Names of the users considered as Capsule users. UserNames []string `json:"userNames,omitempty"` + // Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users) + // // Names of the groups considered as Capsule users. // +kubebuilder:default={capsule.clastix.io} UserGroups []string `json:"userGroups,omitempty"` diff --git a/api/v1beta2/namespace_options.go b/api/v1beta2/namespace_options.go index 5c550be0..cdb19dcb 100644 --- a/api/v1beta2/namespace_options.go +++ b/api/v1beta2/namespace_options.go @@ -11,8 +11,9 @@ type NamespaceOptions struct { // +kubebuilder:validation:Minimum=1 // Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional. Quota *int32 `json:"quota,omitempty"` + // Deprecated: Use additionalMetadataList instead (https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist) + // // Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional. - // Deprecated: Use additionalMetadataList instead AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"` // Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional. AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"` diff --git a/api/v1beta2/tenant_types.go b/api/v1beta2/tenant_types.go index 1037dd6c..b43482f6 100644 --- a/api/v1beta2/tenant_types.go +++ b/api/v1beta2/tenant_types.go @@ -37,11 +37,13 @@ type TenantSpec struct { ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"` // Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional. NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/) + // // Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional. - // Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/) NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"` - // Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional. // Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/) + // + // Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional. LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"` // Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional. ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"` diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index c990987c..0b2c0b14 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -98,6 +98,11 @@ func (in *CapsuleConfigurationList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CapsuleConfigurationSpec) DeepCopyInto(out *CapsuleConfigurationSpec) { *out = *in + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make(api.UserListSpec, len(*in)) + copy(*out, *in) + } if in.UserNames != nil { in, out := &in.UserNames, &out.UserNames *out = make([]string, len(*in)) diff --git a/charts/capsule/README.md b/charts/capsule/README.md index c7e9efac..3270f28c 100644 --- a/charts/capsule/README.md +++ b/charts/capsule/README.md @@ -116,7 +116,7 @@ The following Values have changed key or Value: | manager.options.allowServiceAccountPromotion | bool | `false` | ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts. | | manager.options.annotations | object | `{}` | Additional annotations to add to the CapsuleConfiguration resource | | manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name | -| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Names of the groups considered as Capsule users. | +| manager.options.capsuleUserGroups | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. | | manager.options.createConfiguration | bool | `true` | Create Configuration | | manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash | | manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator | @@ -125,7 +125,8 @@ The following Values have changed key or Value: | manager.options.logLevel | string | `"3"` | Set the log verbosity of the capsule with a value from 1 to 5 | | manager.options.nodeMetadata | object | `{"forbiddenAnnotations":{"denied":[],"deniedRegex":""},"forbiddenLabels":{"denied":[],"deniedRegex":""}}` | Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant | | manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp | -| manager.options.userNames | list | `[]` | Names of the users considered as Capsule users. | +| manager.options.userNames | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. | +| manager.options.users | list | `[{"kind":"Group","name":"projectcapsule.dev"}]` | Define entities which are considered part of the Capsule construct. Users not mentioned here will be ignored by Capsule | | manager.options.workers | int | `1` | Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA). | | manager.rbac.create | bool | `true` | Specifies whether RBAC resources should be created. | | manager.rbac.existingClusterRoles | list | `[]` | Specifies further cluster roles to be added to the Capsule manager service account. | @@ -166,6 +167,13 @@ The following Values have changed key or Value: | Key | Type | Default | Description | |-----|------|---------|-------------| | webhooks.exclusive | bool | `false` | When `crds.exclusive` is `true` the webhooks will be installed | +| webhooks.hooks.config.enabled | bool | `true` | Enable the Hook | +| webhooks.hooks.config.failurePolicy | string | `"Ignore"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) | +| webhooks.hooks.config.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) | +| webhooks.hooks.config.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) | +| webhooks.hooks.config.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) | +| webhooks.hooks.config.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) | +| webhooks.hooks.config.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) | | webhooks.hooks.cordoning.enabled | bool | `true` | Enable the Hook | | webhooks.hooks.cordoning.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) | | webhooks.hooks.cordoning.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) | diff --git a/charts/capsule/ci/tracing-values.yaml b/charts/capsule/ci/tracing.yaml similarity index 100% rename from charts/capsule/ci/tracing-values.yaml rename to charts/capsule/ci/tracing.yaml diff --git a/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml b/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml index b10491ca..4030bc0d 100644 --- a/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml +++ b/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml @@ -158,15 +158,43 @@ spec: userGroups: default: - capsule.clastix.io - description: Names of the groups considered as Capsule users. + description: |- + Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users) + + Names of the groups considered as Capsule users. items: type: string type: array userNames: - description: Names of the users considered as Capsule users. + description: |- + Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users) + + Names of the users considered as Capsule users. items: type: string type: array + users: + description: |- + Define entities which are considered part of the Capsule construct + Users not mentioned here will be ignored by Capsule + items: + properties: + kind: + description: Kind of entity. Possible values are "User", "Group", + and "ServiceAccount" + enum: + - User + - Group + - ServiceAccount + type: string + name: + description: Name of the entity. + type: string + required: + - kind + - name + type: object + type: array required: - enableTLSReconciler type: object diff --git a/charts/capsule/crds/capsule.clastix.io_tenants.yaml b/charts/capsule/crds/capsule.clastix.io_tenants.yaml index 127f54e6..5319d1f8 100644 --- a/charts/capsule/crds/capsule.clastix.io_tenants.yaml +++ b/charts/capsule/crds/capsule.clastix.io_tenants.yaml @@ -129,7 +129,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object imagePullPolicies: @@ -160,7 +163,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object allowedHostnames: @@ -176,7 +182,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object hostnameCollisionScope: @@ -855,7 +864,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object resourceQuotas: @@ -1030,7 +1042,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object required: @@ -1185,7 +1200,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object cordoned: @@ -1203,7 +1221,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string matchExpressions: description: matchExpressions is a list of label selector requirements. @@ -1271,7 +1292,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string default: type: string @@ -1352,7 +1376,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string default: type: string @@ -1412,7 +1439,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string type: object hostnameCollisionScope: @@ -1436,8 +1466,9 @@ spec: type: object limitRanges: description: |- - Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional. Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/) + + Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional. properties: items: items: @@ -1527,8 +1558,9 @@ spec: properties: additionalMetadata: description: |- + Deprecated: Use additionalMetadataList instead (https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist) + Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional. - Deprecated: Use additionalMetadataList instead properties: annotations: additionalProperties: @@ -1642,8 +1674,9 @@ spec: type: object networkPolicies: description: |- - Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional. Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/) + + Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional. properties: items: items: @@ -2292,7 +2325,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string default: type: string @@ -2439,7 +2475,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string default: type: string @@ -2572,7 +2611,10 @@ spec: type: string type: array allowedRegex: - description: Match elements by regex (DEPRECATED) + description: |- + Deprecated: will be removed in a future release + + Match elements by regex. type: string default: type: string diff --git a/charts/capsule/templates/configuration.yaml b/charts/capsule/templates/configuration.yaml index d6a4cb2b..29d937cf 100644 --- a/charts/capsule/templates/configuration.yaml +++ b/charts/capsule/templates/configuration.yaml @@ -14,6 +14,10 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: + administrators: + {{- toYaml .Values.manager.options.administrators | nindent 4 }} + users: + {{- toYaml .Values.manager.options.users | nindent 4 }} enableTLSReconciler: {{ .Values.tls.enableController }} overrides: mutatingWebhookConfigurationName: {{ include "capsule.fullname" . }}-mutating-webhook-configuration diff --git a/charts/capsule/templates/crd-lifecycle/job.yaml b/charts/capsule/templates/crd-lifecycle/job.yaml index 3e1935a9..50867673 100644 --- a/charts/capsule/templates/crd-lifecycle/job.yaml +++ b/charts/capsule/templates/crd-lifecycle/job.yaml @@ -87,7 +87,7 @@ spec: # piping stderr to stdout means kubectl's errors are surfaced # in the pod's logs. - kubectl apply --server-side=true --overwrite=true --force-conflicts=true -f /data/ 2>&1 + kubectl apply --server-side=true --overwrite=true --force-conflicts=true --field-manager='capsule/crd-lifecycle' -f /data/ 2>&1 volumeMounts: {{- range $path, $_ := .Files.Glob "crds/**.yaml" }} - name: {{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }} diff --git a/charts/capsule/templates/validatingwebhookconfiguration.yaml b/charts/capsule/templates/validatingwebhookconfiguration.yaml index a192b618..0095f609 100644 --- a/charts/capsule/templates/validatingwebhookconfiguration.yaml +++ b/charts/capsule/templates/validatingwebhookconfiguration.yaml @@ -603,4 +603,40 @@ webhooks: timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }} {{- end }} {{- end }} +{{- with .Values.webhooks.hooks.config }} + {{- if .enabled }} +- name: config.projectcapsule.dev + admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + {{- include "capsule.webhooks.service" (dict "path" "/config/validating" "ctx" $) | nindent 4 }} + failurePolicy: {{ .failurePolicy }} + matchPolicy: {{ .matchPolicy }} + {{- with .namespaceSelector }} + namespaceSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .objectSelector }} + objectSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .matchConditions }} + matchConditions: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - apiGroups: + - capsule.clastix.io + apiVersions: + - v1beta2 + operations: + - UPDATE + resources: + - capsuleconfigurations + scope: 'Cluster' + sideEffects: None + timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }} + {{- end }} +{{- end }} {{- end }} diff --git a/charts/capsule/values.schema.json b/charts/capsule/values.schema.json index 2725227d..de41c0f7 100644 --- a/charts/capsule/values.schema.json +++ b/charts/capsule/values.schema.json @@ -336,11 +336,8 @@ "type": "string" }, "capsuleUserGroups": { - "description": "Names of the groups considered as Capsule users.", - "type": "array", - "items": { - "type": "string" - } + "description": "DEPRECATED: use users properties. Names of the users considered as Capsule users.", + "type": "array" }, "createConfiguration": { "description": "Create Configuration", @@ -399,9 +396,24 @@ "type": "string" }, "userNames": { - "description": "Names of the users considered as Capsule users.", + "description": "DEPRECATED: use users properties. Names of the users considered as Capsule users.", "type": "array" }, + "users": { + "description": "Define entities which are considered part of the Capsule construct. Users not mentioned here will be ignored by Capsule", + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }, "workers": { "description": "Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA).", "type": "integer" @@ -739,6 +751,39 @@ "hooks": { "type": "object", "properties": { + "config": { + "type": "object", + "properties": { + "enabled": { + "description": "Enable the Hook", + "type": "boolean" + }, + "failurePolicy": { + "description": "[FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)", + "type": "string" + }, + "matchConditions": { + "description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)", + "type": "array" + }, + "matchPolicy": { + "description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)", + "type": "string" + }, + "namespaceSelector": { + "description": "[NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)", + "type": "object" + }, + "objectSelector": { + "description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)", + "type": "object" + }, + "reinvocationPolicy": { + "description": "[ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)", + "type": "string" + } + } + }, "cordoning": { "type": "object", "properties": { diff --git a/charts/capsule/values.yaml b/charts/capsule/values.yaml index 6cf51092..e863f02e 100644 --- a/charts/capsule/values.yaml +++ b/charts/capsule/values.yaml @@ -178,6 +178,11 @@ manager: workers: 1 # -- Set the log verbosity of the capsule with a value from 1 to 5 logLevel: '3' + # -- Define entities which are considered part of the Capsule construct. + # Users not mentioned here will be ignored by Capsule + users: + - kind: "Group" + name: "projectcapsule.dev" # -- Define entities which can act as Administrators in the capsule construct # These entities are automatically owners for all existing tenants. Meaning they can add namespaces to any tenant. However they must be specific by using the capsule label # for interacting with namespaces. Because if that label is not defined, it's assumed that namespace interaction was not targeted towards a tenant and will therefor @@ -185,10 +190,6 @@ manager: administrators: [] # - kind: User # name: alice - # -- Names of the users considered as Capsule users. - userNames: [] - # -- Names of the groups considered as Capsule users. - capsuleUserGroups: ["projectcapsule.dev"] # -- Define groups which when found in the request of a user will be ignored by the Capsule # this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups. ignoreUserWithGroups: [] @@ -210,6 +211,13 @@ manager: forbiddenAnnotations: denied: [] deniedRegex: "" + # -- DEPRECATED: use users properties. + # Names of the users considered as Capsule users. + userNames: [] + # -- DEPRECATED: use users properties. + # Names of the users considered as Capsule users. + capsuleUserGroups: [] + # -- A list of extra arguments for the capsule controller extraArgs: @@ -654,6 +662,22 @@ webhooks: # -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) reinvocationPolicy: Never + config: + # -- Enable the Hook + enabled: true + # -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) + failurePolicy: Ignore + # -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) + matchPolicy: Exact + # -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) + objectSelector: {} + # -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) + namespaceSelector: {} + # -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) + matchConditions: [] + # -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) + reinvocationPolicy: Never + tenantResourceObjects: # -- Enable the Hook enabled: true diff --git a/cmd/main.go b/cmd/main.go index 4e4c638a..dc31c824 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -43,6 +43,7 @@ import ( utilscontroller "github.com/projectcapsule/capsule/internal/controllers/utils" "github.com/projectcapsule/capsule/internal/metrics" "github.com/projectcapsule/capsule/internal/webhook" + cfgvalidation "github.com/projectcapsule/capsule/internal/webhook/cfg" "github.com/projectcapsule/capsule/internal/webhook/defaults" "github.com/projectcapsule/capsule/internal/webhook/dra" "github.com/projectcapsule/capsule/internal/webhook/gateway" @@ -88,11 +89,11 @@ func printVersion() { setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", goRuntime.GOOS, goRuntime.GOARCH)) } -//nolint:maintidx +//nolint:maintidx,cyclop func main() { controllerConfig := utilscontroller.ControllerOptions{} - var enableLeaderElection, version bool + var enableLeaderElection, enablePprof, version bool var metricsAddr, ns string @@ -108,6 +109,7 @@ func main() { "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&version, "version", false, "Print the Capsule version and exit") flag.StringVar(&controllerConfig.ConfigurationName, "configuration-name", "default", "The CapsuleConfiguration resource name to use") + flag.BoolVar(&enablePprof, "enable-pprof", false, "Enables Pprof endpoint for profiling (not recommend in production)") opts := zap.Options{ EncoderConfigOptions: append([]zap.EncoderConfigOption{}, func(config *zapcore.EncoderConfig) { @@ -139,7 +141,7 @@ func main() { os.Exit(1) } - manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + ctrlOpts := ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ BindAddress: metricsAddr, @@ -155,7 +157,13 @@ func main() { return client.New(config, options) }, - }) + } + + if enablePprof { + ctrlOpts.PprofBindAddress = ":8082" + } + + manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrlOpts) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) @@ -311,6 +319,9 @@ func main() { route.TenantAssignment( misc.TenantAssignmentHandler(), ), + route.ConfigValidation( + cfgvalidation.WarningHandler(), + ), ) nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion) diff --git a/e2e/custom_capsule_group_test.go b/e2e/custom_capsule_group_test.go index 45a22090..0d672271 100644 --- a/e2e/custom_capsule_group_test.go +++ b/e2e/custom_capsule_group_test.go @@ -69,7 +69,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should fail using a User non matching the capsule-user-group flag", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { - configuration.Spec.UserGroups = []string{"test"} + configuration.Spec.UserNames = []string{} + configuration.Spec.UserGroups = []string{} + configuration.Spec.Users = []api.UserSpec{{Kind: api.GroupOwner, Name: "test"}} }) ns := NewNamespace("") @@ -78,7 +80,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should succeed and be available in Tenant namespaces list with multiple groups", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { - configuration.Spec.UserGroups = []string{"test", "alice"} + configuration.Spec.UserNames = []string{} + configuration.Spec.UserGroups = []string{} + configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: "alice"}, {Kind: api.GroupOwner, Name: "test"}} }) ns := NewNamespace("") @@ -89,7 +93,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should succeed and be available in Tenant namespaces list with default single group", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { - configuration.Spec.UserGroups = []string{"projectcapsule.dev"} + configuration.Spec.UserNames = []string{} + configuration.Spec.UserGroups = []string{} + configuration.Spec.Users = []api.UserSpec{{Kind: api.GroupOwner, Name: "projectcapsule.dev"}} }) ns := NewNamespace("") @@ -100,7 +106,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should fail when group is ignored", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { - configuration.Spec.UserGroups = []string{"projectcapsule.dev"} + configuration.Spec.UserNames = []string{} + configuration.Spec.UserGroups = []string{} + configuration.Spec.Users = []api.UserSpec{{Kind: api.GroupOwner, Name: "projectcapsule.dev"}} configuration.Spec.IgnoreUserWithGroups = []string{"projectcapsule.dev"} }) @@ -111,9 +119,10 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should succeed and be available in Tenant namespaces list with default single user", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { + configuration.Spec.UserNames = []string{} configuration.Spec.UserGroups = []string{} + configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: tnt.Spec.Owners[0].Name}} configuration.Spec.IgnoreUserWithGroups = []string{} - configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name} }) ns := NewNamespace("") @@ -123,9 +132,10 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should succeed and be available in Tenant namespaces list with default single user", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { + configuration.Spec.UserNames = []string{} configuration.Spec.UserGroups = []string{} configuration.Spec.IgnoreUserWithGroups = []string{} - configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name} + configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: tnt.Spec.Owners[0].Name}} }) ns := NewNamespace("") @@ -135,8 +145,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should fail when group is ignored", func() { ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) { + configuration.Spec.UserNames = []string{} configuration.Spec.UserGroups = []string{} - configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name} + configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: tnt.Spec.Owners[0].Name}} configuration.Spec.IgnoreUserWithGroups = []string{"projectcapsule.dev"} }) diff --git a/hack/distro/fluxcd/kustomization.yaml b/hack/distro/fluxcd/kustomization.yaml new file mode 100644 index 00000000..8a8c2444 --- /dev/null +++ b/hack/distro/fluxcd/kustomization.yaml @@ -0,0 +1,33 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml +patches: + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --no-cross-namespace-refs=true + target: + kind: Deployment + name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)" + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --no-remote-bases=true + target: + kind: Deployment + name: "kustomize-controller" + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --default-service-account=default + target: + kind: Deployment + name: "(kustomize-controller|helm-controller)" + - patch: | + - op: replace + path: /spec/replicas + value: 0 + target: + kind: Deployment + name: "(notification-controller|image-reflector-controller|image-automation-controller)" diff --git a/hack/distro/host-proxy/deploy.yaml b/hack/distro/host-proxy/deploy.yaml new file mode 100644 index 00000000..3ccc7053 --- /dev/null +++ b/hack/distro/host-proxy/deploy.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capsule + namespace: monitoring-system + labels: + app: capsule +spec: + replicas: 1 + selector: + matchLabels: + app: capsule + template: + metadata: + labels: + app: capsule + annotations: + prometheus.io/scrape: 'true' + prometheus.io/path: '/metrics' + prometheus.io/port: '8080' + profiles.grafana.com/memory.scrape: "true" + profiles.grafana.com/memory.port: "8082" + profiles.grafana.com/cpu.scrape: "true" + profiles.grafana.com/cpu.port: "8082" + profiles.grafana.com/goroutine.scrape: "true" + profiles.grafana.com/goroutine.port: "8082" + spec: + containers: + - name: tcp-proxy + image: alpine/socat + ports: + - containerPort: 8080 + - containerPort: 8082 + command: ["sh", "-c"] + args: + - | + set -e + echo "Starting TCP proxy to ${LAPTOP_HOST_IP}..." + # Forward 8080 -> TARGET_HOST:8080 + socat TCP-LISTEN:8080,fork,reuseaddr TCP:${LAPTOP_HOST_IP}:8080 & + # Forward 8082 -> TARGET_HOST:8082 + socat TCP-LISTEN:8082,fork,reuseaddr TCP:${LAPTOP_HOST_IP}:8082 & + wait diff --git a/hack/distro/host-proxy/kustomization.yaml b/hack/distro/host-proxy/kustomization.yaml new file mode 100644 index 00000000..33c25b62 --- /dev/null +++ b/hack/distro/host-proxy/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deploy.yaml + - servicemonitor.yaml diff --git a/hack/distro/host-proxy/servicemonitor.yaml b/hack/distro/host-proxy/servicemonitor.yaml new file mode 100644 index 00000000..f4994c3c --- /dev/null +++ b/hack/distro/host-proxy/servicemonitor.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: capsule + namespace: monitoring-system + labels: + app: capsule + release: kube-prometheus-stack +spec: + selector: + app: capsule + ports: + - name: metrics + port: 8080 + targetPort: 8080 + protocol: TCP +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: capsule + namespace: monitoring-system + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app: capsule + endpoints: + - port: metrics + path: /metrics + interval: 5s diff --git a/hack/distro/monitoring/dashboards/ctrl-runtime-admission.json b/hack/distro/monitoring/dashboards/ctrl-runtime-admission.json new file mode 100644 index 00000000..f258d260 --- /dev/null +++ b/hack/distro/monitoring/dashboards/ctrl-runtime-admission.json @@ -0,0 +1,515 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Profiling performance-related metrics based on controller-runtime.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 10, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 6, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "controller_runtime_active_workers{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{controller}}", + "range": true, + "refId": "A" + } + ], + "title": "Active Workers", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum by (result) (rate(controller_runtime_reconcile_total{job=~\"$job\"}[$__rate_interval]))", + "interval": "", + "legendFormat": "{{result}}", + "range": true, + "refId": "A" + } + ], + "title": "Reoncile Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 8, + "panels": [], + "repeat": "Webhook", + "title": "Webhook \"$Webhook\" Status", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(controller_runtime_webhook_latency_seconds_count{job=~\"$job\", webhook=\"$Webhook\"}[$__rate_interval])", + "interval": "", + "legendFormat": "Request Rate", + "range": true, + "refId": "A" + } + ], + "title": "Webhook Request Rate $Webhook", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 12, + "options": { + "calculate": false, + "calculation": {}, + "cellGap": 2, + "cellValues": {}, + "color": { + "exponent": 0.5, + "fill": "#b4ff00", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 128 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false + }, + "rowsFrame": { + "layout": "auto" + }, + "showValue": "never", + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "short" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(controller_runtime_webhook_latency_seconds_bucket{job=~\"$job\", webhook=\"$Webhook\"}[$__rate_interval])", + "format": "heatmap", + "interval": "", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile Time Buckets", + "type": "heatmap" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": [ + "capsule" + ], + "value": [ + "capsule" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(controller_runtime_webhook_requests_total,job)", + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(controller_runtime_webhook_requests_total,job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": { + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(controller_runtime_webhook_requests_total{job=~\"$job\"},webhook)", + "includeAll": true, + "multi": true, + "name": "Webhook", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(controller_runtime_webhook_requests_total{job=~\"$job\"},webhook)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Controller Runtime Webhooks Detail", + "uid": "0L6Y8KEnk", + "version": 1 +} diff --git a/hack/distro/monitoring/dashboards/ctrl-runtime.json b/hack/distro/monitoring/dashboards/ctrl-runtime.json new file mode 100644 index 00000000..f104c4af --- /dev/null +++ b/hack/distro/monitoring/dashboards/ctrl-runtime.json @@ -0,0 +1,546 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Profiling performance-related metrics based on controller-runtime.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 22, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 6, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "controller_runtime_active_workers{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{controller}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum (controller_runtime_active_workers{job=~\"$job\"})", + "hide": false, + "instant": false, + "legendFormat": "Total", + "range": true, + "refId": "B" + } + ], + "title": "Active Workers", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (result) (\n increase(controller_runtime_reconcile_total{job=~\"$job\"}[$__rate_interval])\n)", + "instant": false, + "interval": "", + "legendFormat": "{{result}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(\n increase(controller_runtime_reconcile_total{job=~\"$job\"}[$__rate_interval])\n)", + "hide": false, + "instant": false, + "legendFormat": "Total", + "range": true, + "refId": "B" + } + ], + "title": "Reoncile Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 8, + "panels": [], + "repeat": "Controller", + "title": "Controller \"$Controller\" Status", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum by (result) (\n increase(controller_runtime_reconcile_total{job=~\"$job\", controller=~\"$Controller\"}[$__rate_interval])\n)\n", + "interval": "", + "legendFormat": "{{result}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 12, + "options": { + "calculate": false, + "calculation": {}, + "cellGap": 2, + "cellValues": {}, + "color": { + "exponent": 0.5, + "fill": "#b4ff00", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 128 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false + }, + "rowsFrame": { + "layout": "auto" + }, + "showValue": "never", + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "short" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(controller_runtime_reconcile_time_seconds_bucket{job=~'$job', controller=~'$Controller'}[$__rate_interval])", + "format": "heatmap", + "interval": "", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile Time Buckets", + "type": "heatmap" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(controller_runtime_active_workers,job)", + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(controller_runtime_active_workers,job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(controller_runtime_active_workers{job=~\"$job\"},controller)", + "includeAll": true, + "multi": true, + "name": "Controller", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(controller_runtime_active_workers{job=~\"$job\"},controller)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Controller Runtime Controllers Detail", + "uid": "5J4pyKEnk", + "version": 1 +} diff --git a/hack/distro/monitoring/dashboards/kustomization.yaml b/hack/distro/monitoring/dashboards/kustomization.yaml new file mode 100644 index 00000000..4ec685d4 --- /dev/null +++ b/hack/distro/monitoring/dashboards/kustomization.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: monitoring-system +commonLabels: + release: prometheus +generatorOptions: + annotations: + k8s-sidecar-target-directory: /tmp/dashboards/Controller Development + labels: + grafana_dashboard: "1" + disableNameSuffixHash: true +configMapGenerator: +- name: ctrl-runtime + files: + - ctrl-runtime.json +- name: ctrl-runtime-admission + files: + - ctrl-runtime-admission.json +- name: perf + files: + - performance.json diff --git a/hack/distro/monitoring/dashboards/performance.json b/hack/distro/monitoring/dashboards/performance.json new file mode 100644 index 00000000..6ad18ae6 --- /dev/null +++ b/hack/distro/monitoring/dashboards/performance.json @@ -0,0 +1,4466 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "This production-ready dashboard provides full-stack visibility for Golang applications using Echo framework and GORM ORM. Monitor critical application metrics including HTTP performance, memory allocation patterns, garbage collection efficiency, and database connection pooling. Features intelligent Apdex scoring, live request tracing, and resource utilization trends with built-in alert thresholds. Perfect for DevOps teams managing high-performance Golang microservices.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 33, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "Down" + }, + "1": { + "color": "green", + "index": 1, + "text": "Up" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "bool" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "up{job=~\"$job\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 2 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 2, + "y": 0 + }, + "id": 8, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(changes(process_start_time_seconds{instance=~\"$instance\"}[$interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Restarts in $interval", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "orange", + "value": 70 + }, + { + "color": "green", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 5, + "y": 0 + }, + "id": 2, + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "(\n (\n sum(echo_request_duration_seconds_bucket{le=\"$target\", code=~\"^2..$\", instance=~\"$instance\"})\n + (\n sum(echo_request_duration_seconds_bucket{le=\"$tolerated\", code=~\"^2..$\", instance=~\"$instance\"})\n - sum(echo_request_duration_seconds_bucket{le=\"$target\", code=~\"^2..$\", instance=~\"$instance\"})\n ) / 2\n ) / sum(echo_request_duration_seconds_count{instance=~\"$instance\", code=~\"^2..$\"})\n) * 100", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Apdex Score: $target s, $tolerated s", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "red", + "value": 200 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 9, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(echo_request_duration_seconds_count{instance=~\"$instance\"}[$interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "QPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 11, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(echo_request_duration_seconds_count{instance=~\"$instance\"})", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Requests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 5 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 13, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "(\n sum(echo_request_duration_seconds_count{instance=~\"$instance\", code=~\"^[45]..$\"} OR on() vector(0))\n / sum(echo_request_duration_seconds_count{instance=~\"$instance\"})\n) * 100", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Error", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 15, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(echo_request_size_bytes_sum{instance=~\"$instance\"}) + sum(echo_response_size_bytes_sum{instance=~\"$instance\"})", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Transferred", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 17, + "y": 0 + }, + "id": 7, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(echo_request_size_bytes_sum{instance=~\"$instance\"}[$interval])) + sum(rate(echo_response_size_bytes_sum{instance=~\"$instance\"}[$interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Transfer Rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 20, + "y": 0 + }, + "id": 24, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg by(instance)(go_goroutines{instance=~\"$instance\"})", + "format": "time_series", + "instant": false, + "legendFormat": "{{instance}}: goroutines (avg)", + "range": true, + "refId": "A" + } + ], + "title": "Goroutines", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 22, + "y": 0 + }, + "id": 25, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg(go_threads{instance=~\"$instance\"}) by(instance)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Threads", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 9, + "panels": [], + "title": "Go Memory", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 4 + }, + "id": 13, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_sys_bytes{instance=~\"$instance\"})", + "legendFormat": "{{instance}} (avg)", + "range": true, + "refId": "AVG" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_sys_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}} (deriv)", + "range": true, + "refId": "DERIVATIVE" + } + ], + "title": "Total Reserved Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 14, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_stack_sys_bytes{instance=~\"$instance\"})", + "legendFormat": "{{instance}}: stack in use (avg)", + "range": true, + "refId": "AVG" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_stack_inuse_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: stack in use (deriv)", + "range": true, + "refId": "DERIVATIVE" + } + ], + "title": "Stack Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 15, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_mspan_sys_bytes{instance=~\"$instance\"})", + "legendFormat": "{{instance}}: mspan (avg)", + "range": true, + "refId": "MSPAN (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_mspan_sys_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: mspan (deriv)", + "range": true, + "refId": "MSPAN (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_mcache_sys_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: mcache (avg)", + "range": true, + "refId": "MCACHE (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_mcache_sys_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: mcache (deriv)", + "range": true, + "refId": "MCACHE (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_buck_hash_sys_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: buck hash (avg)", + "range": true, + "refId": "BUCK HASH (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_buck_hash_sys_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: buck hash (deriv)", + "range": true, + "refId": "BUCK HASH (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_gc_sys_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: gc (avg)", + "range": true, + "refId": "GC (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_gc_sys_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: gc (deriv)", + "range": true, + "refId": "GC (DERIV)" + } + ], + "title": "Other Memory Reservations", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 16, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_heap_sys_bytes{instance=~\"$instance\"})", + "legendFormat": "{{instance}}: heap reserved (avg)", + "range": true, + "refId": "SYS (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_heap_sys_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap reserved (deriv)", + "range": true, + "refId": "SYS (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_heap_inuse_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap in use (avg)", + "range": true, + "refId": "INUSE (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_heap_inuse_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap in use (deriv)", + "range": true, + "refId": "INUSE (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_heap_alloc_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap alloc (avg)", + "range": true, + "refId": "ALLOC (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_heap_alloc_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap alloc (deriv)", + "range": true, + "refId": "ALLOC (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_heap_idle_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap idle (avg)", + "range": true, + "refId": "IDLE (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_heap_idle_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap idle (deriv)", + "range": true, + "refId": "IDLE (DERIV)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_heap_released_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap released (avg)", + "range": true, + "refId": "RELEASED (AVG)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(go_memstats_heap_released_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: heap released (deriv)", + "range": true, + "refId": "RELEASED (DERIV)" + } + ], + "title": "Heap Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 20 + }, + "id": 44, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Spectral", + "steps": 128 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "bytes" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(le) (rate(go_gc_heap_allocs_by_size_bytes_bucket{instance=~\"$instance\"}[$__rate_interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Heap Alloc By Size", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 45, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Spectral", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "bytes" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(le)(rate(go_gc_heap_frees_by_size_bytes_bucket{instance=~\"$instance\"}[$__rate_interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Heap Frees By Size", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 17, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(rate(go_memstats_alloc_bytes_total{instance=~\"$instance\"}[$__rate_interval]))", + "legendFormat": "{{instance}}: bytes malloced/s (avg)", + "range": true, + "refId": "A" + } + ], + "title": "Allocation Rate, Bytes (IEC)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "cps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(go_memstats_mallocs_total{instance=~\"$instance\"}[$__rate_interval])", + "legendFormat": "{{instance}}: obj mallocs/s", + "range": true, + "refId": "A" + } + ], + "title": "Heap Object Allocation Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 19, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(\n go_memstats_mallocs_total{instance=~\"$instance\"}\n - go_memstats_frees_total{instance=~\"$instance\"}\n)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Number of Live Objects", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(process_resident_memory_bytes{instance=~\"$instance\"})", + "legendFormat": "{{instance}}: resident memory (avg)", + "range": true, + "refId": "RESIDENT AVG" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(hypr_process_resident_memory_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: resident memory (deriv)", + "range": true, + "refId": "RESIDENT DERIV" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(hypr_process_virtual_memory_bytes{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: virtual memory (avg)", + "range": true, + "refId": "VIRTUAL AVG" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "deriv(hypr_process_virtual_memory_bytes{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: virtual memory (deriv)", + "range": true, + "refId": "VIRTUAL DERIV" + } + ], + "title": "Process Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 10, + "panels": [], + "title": "Go Garbage collector", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_gc_duration_seconds{quantile=\"0.0\", instance=~\"$instance\"})", + "legendFormat": "{{instance}}: min gc time (avg)", + "range": true, + "refId": "MIN" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_gc_duration_seconds{quantile=\"1.0\", instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: max gc time (avg)", + "range": true, + "refId": "MAX" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_gc_duration_seconds{instance=~\"$instance\"})", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: gc duration (avg)", + "range": true, + "refId": "DURATION" + } + ], + "title": "GC min & max duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 21, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg by(instance)(go_memstats_next_gc_bytes{instance=~\"$instance\"})", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Next GC, Bytes (IEC)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 43, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Spectral", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "s" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(le)(rate(go_gc_pauses_seconds_bucket{instance=~\"$instance\"}[$__rate_interval]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GC Pauses", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [ + { + "options": { + "null": { + "index": 0, + "text": "N/A" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(go_memstats_lookups_total{instance=~\"$instance\"}) by (instance)", + "legendFormat": "{{instance}}: pointer deferences (avg)", + "range": true, + "refId": "A" + } + ], + "title": "Pointer Deferences", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 61 + }, + "id": 26, + "panels": [], + "title": "Go File Descriptors", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 17, + "x": 0, + "y": 62 + }, + "id": 27, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(process_open_fds{instance=~\"$instance\"}) by (instance)", + "legendFormat": "{{instance}} (avg)", + "range": true, + "refId": "AVG" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "deriv(process_open_fds{instance=~\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "{{instance}} (deriv)", + "range": true, + "refId": "DERIV" + } + ], + "title": "Open File Descriptors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": false, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "#EAB839", + "value": 80 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 17, + "y": 62 + }, + "id": 28, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^open_fds$/", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg(process_open_fds{instance=~\"$instance\"}) by (instance)", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "OPEN" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(process_max_fds{instance=~\"$instance\"}) by (instance)", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "MAX" + } + ], + "title": "File Descriptor Usage", + "transformations": [ + { + "filter": { + "id": "byRefId", + "options": "/^(?:OPEN|MAX)$/" + }, + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "last" + ] + }, + "topic": "series" + }, + { + "filter": { + "id": "byRefId", + "options": "/^(?:OPEN)$/" + }, + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "host.docker.internal:8080": "open_fds" + } + }, + "topic": "series" + }, + { + "filter": { + "id": "byRefId", + "options": "/^(?:MAX)$/" + }, + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "host.docker.internal:8080": "max_fds" + } + }, + "topic": "series" + } + ], + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 11, + "panels": [], + "title": "Echo Framework", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 11, + "x": 0, + "y": 71 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(\n (\n sum(echo_request_duration_seconds_bucket{le=\"$target\", code=~\"^2..$\", instance=~\"$instance\"}) by (instance)\n + (\n sum(echo_request_duration_seconds_bucket{le=\"$tolerated\", code=~\"^2..$\", instance=~\"$instance\"}) by (instance)\n - sum(echo_request_duration_seconds_bucket{le=\"$target\", code=~\"^2..$\", instance=~\"$instance\"}) by (instance)\n ) / 2\n ) / sum(echo_request_duration_seconds_count{instance=~\"$instance\", code=~\"^2..$\"}) by (instance)\n) * 100", + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Apdex Score: $target s, $tolerated s", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 13, + "x": 11, + "y": 71 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(echo_request_duration_seconds_bucket{instance=~\"$instance\"}[$__rate_interval])) by (code, le))", + "legendFormat": "{{code}} (95%)", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(1, sum(rate(echo_request_duration_seconds_bucket{instance=~\"$instance\"}[$__rate_interval])) by (code, le))", + "hide": false, + "instant": false, + "legendFormat": "{{code}} (100%)", + "range": true, + "refId": "B" + } + ], + "title": "Response Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 11, + "x": 0, + "y": 77 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(echo_request_duration_seconds_count{instance=~\"$instance\"}[$__rate_interval])) by (code)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Requests/s", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 11, + "x": 0, + "y": 83 + }, + "id": 33, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(echo_request_duration_seconds_count{instance=~\"$instance\"}) by (code)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Code Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 13, + "x": 11, + "y": 83 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(echo_request_size_bytes_sum{instance=~\"$instance\"}) by (code, method, url)", + "legendFormat": "{{method}} {{url}} {{code}}", + "range": true, + "refId": "A" + } + ], + "title": "Response Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 11, + "x": 0, + "y": 89 + }, + "id": 34, + "options": { + "legend": { + "calcs": [ + "last" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(echo_request_duration_seconds_count{instance=~\"$instance\"}) by (url)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Request Count", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "footer": { + "reducers": [] + }, + "inspect": false, + "minWidth": 50, + "wrapText": false + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 95 + }, + "id": 35, + "options": { + "cellHeight": "sm", + "enablePagination": false, + "frameIndex": 1, + "showHeader": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, sum(echo_request_duration_seconds_sum{instance=~\"$instance\"}) by (url))", + "format": "table", + "instant": true, + "interval": "1", + "legendFormat": "{{url}}", + "range": false, + "refId": "A" + } + ], + "title": "Top 5 Request Duration", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "byVariable": false, + "include": { + "names": [ + "Value", + "url" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Duration", + "url": "Route" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "footer": { + "reducers": [] + }, + "inspect": false, + "minWidth": 50, + "wrapText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 95 + }, + "id": 36, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, sum(echo_request_duration_seconds_sum{instance=~\"$instance\", code=~\"^[23]..$\"}) by (url))", + "format": "table", + "instant": true, + "interval": "1", + "legendFormat": "{{url}}", + "range": false, + "refId": "A" + } + ], + "title": "Top 5 Success Duration", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "url", + "Value" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Duration", + "url": "Route" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "footer": { + "reducers": [] + }, + "inspect": false, + "minWidth": 50, + "wrapText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 95 + }, + "id": 37, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, sum(echo_request_duration_seconds_sum{instance=~\"$instance\", code=~\"^[54]..$\"}) by (url))", + "format": "table", + "instant": true, + "interval": "1", + "legendFormat": "{{url}}", + "range": false, + "refId": "A" + } + ], + "title": "Top 5 Error Duration", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "url", + "Value" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Duration", + "url": "Route" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "footer": { + "reducers": [] + }, + "inspect": false, + "minWidth": 50, + "wrapText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 95 + }, + "id": 38, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, sum(echo_request_duration_seconds_count{instance=~\"$instance\"}) by (url))", + "format": "table", + "instant": true, + "legendFormat": "{{url}}", + "range": false, + "refId": "A" + } + ], + "title": "Top 5 Request Count", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "url", + "Value" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "url": "Route" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "footer": { + "reducers": [] + }, + "inspect": false, + "minWidth": 50, + "wrapText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 95 + }, + "id": 39, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, sum(echo_request_duration_seconds_count{instance=~\"$instance\", code=~\"^[23]..$\"}) by (url))", + "format": "table", + "instant": true, + "legendFormat": "{{url}}", + "range": false, + "refId": "A" + } + ], + "title": "Top 5 Success Count", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "url", + "Value" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "url": "Route" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "footer": { + "reducers": [] + }, + "inspect": false, + "minWidth": 50, + "wrapText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 20, + "y": 95 + }, + "id": 40, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, sum(echo_request_duration_seconds_count{instance=~\"$instance\", code=~\"^[54]..$\"}) by (url))", + "format": "table", + "instant": true, + "legendFormat": "{{url}}", + "range": false, + "refId": "A" + } + ], + "title": "Top 5 Error Count", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "url", + "Value" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "url": "Route" + } + } + } + ], + "type": "table" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 101 + }, + "id": 12, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": false, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "#EAB839", + "value": 85 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 792 + }, + "id": 41, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^open_conn$/", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(go_sql_max_open_connections{instance=~\"$instance\"}) by (db_name)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg(go_sql_open_connections{instance=~\"$instance\"}) by (db_name)", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "DB Connections", + "transformations": [ + { + "filter": { + "id": "byRefId", + "options": "/^(?:A|B)$/" + }, + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "last" + ] + }, + "topic": "series" + }, + { + "filter": { + "id": "byRefId", + "options": "/^(?:A)$/" + }, + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "academy": "max_conn" + } + }, + "topic": "series" + }, + { + "filter": { + "id": "byRefId", + "options": "/^(?:B)$/" + }, + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "academy": "open_conn" + } + }, + "topic": "series" + } + ], + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "#EAB839", + "value": 80 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 21, + "x": 3, + "y": 792 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg(go_sql_idle_connections{instance=~\"$instance\"}) by (instance, db_name)", + "legendFormat": "{{instance}}: {{db_name}} idle connections (avg)", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(go_sql_in_use_connections{instance=~\"$instance\"}) by (instance, db_name)", + "hide": false, + "instant": false, + "legendFormat": "{{instance}}: {{db_name}} in use connections (avg)", + "range": true, + "refId": "B" + } + ], + "title": "Database Connections", + "type": "timeseries" + } + ], + "title": "GORM", + "type": "row" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "allowCustomValue": false, + "current": { + "text": "Prometheus", + "value": "prometheus" + }, + "description": "Variable holding the origin of the data", + "includeAll": false, + "label": "Datasource", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allowCustomValue": false, + "current": { + "text": "capsule", + "value": "capsule" + }, + "definition": "label_values(job)", + "label": "Job", + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allowCustomValue": false, + "current": { + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "definition": "label_values({job=\"$job\"},instance)", + "description": "", + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "qryType": 1, + "query": "label_values({job=\"$job\"},instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "0.05", + "value": "0.05" + }, + "label": "Target", + "name": "target", + "options": [ + { + "selected": true, + "text": "0.05", + "value": "0.05" + }, + { + "selected": false, + "text": "0.1", + "value": "0.1" + }, + { + "selected": false, + "text": "0.2", + "value": "0.2" + }, + { + "selected": false, + "text": "0.3", + "value": "0.3" + }, + { + "selected": false, + "text": "0.4", + "value": "0.4" + }, + { + "selected": false, + "text": "0.5", + "value": "0.5" + } + ], + "query": "0.05,0.1,0.2,0.3,0.4,0.5", + "type": "custom" + }, + { + "current": { + "text": "0.1", + "value": "0.1" + }, + "label": "Tolerated", + "name": "tolerated", + "options": [ + { + "selected": true, + "text": "0.1", + "value": "0.1" + }, + { + "selected": false, + "text": "0.2", + "value": "0.2" + }, + { + "selected": false, + "text": "0.3", + "value": "0.3" + }, + { + "selected": false, + "text": "0.4", + "value": "0.4" + }, + { + "selected": false, + "text": "0.5", + "value": "0.5" + } + ], + "query": "0.1,0.2,0.3,0.4,0.5", + "type": "custom" + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "1d", + "value": "1d" + }, + "label": "Interval", + "name": "interval", + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": true, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Golang Monitoring - Echo & GORM", + "uid": "een1r1w7eo6bke", + "version": 3 +} diff --git a/hack/distro/monitoring/kube-prometheus-stack/kustomization.yaml b/hack/distro/monitoring/kube-prometheus-stack/kustomization.yaml new file mode 100644 index 00000000..7fcbf108 --- /dev/null +++ b/hack/distro/monitoring/kube-prometheus-stack/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - release.flux.yaml diff --git a/hack/distro/monitoring/kube-prometheus-stack/release.flux.yaml b/hack/distro/monitoring/kube-prometheus-stack/release.flux.yaml new file mode 100644 index 00000000..812220e4 --- /dev/null +++ b/hack/distro/monitoring/kube-prometheus-stack/release.flux.yaml @@ -0,0 +1,100 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: kube-prometheus-stack + namespace: flux-system +spec: + serviceAccountName: kustomize-controller + interval: 30s + timeout: 10m + targetNamespace: monitoring-system + releaseName: "kube-prometheus-stack" + chart: + spec: + chart: kube-prometheus-stack + version: "79.11.0" + sourceRef: + kind: HelmRepository + name: kube-prometheus-stack + interval: 24h + install: + createNamespace: true + remediation: + retries: -1 + upgrade: + remediation: + remediateLastFailure: true + retries: -1 + driftDetection: + mode: enabled + values: + grafana: + additionalDataSources: + - name: Pyroscope + type: grafana-pyroscope-datasource + uid: pyroscope + url: http://pyroscope.{{ $.Release.Namespace }}.svc.cluster.local.:4040/ + adminPassword: admin + global: + dnsService: "kube-dns" + dnsNamespace: "kube-system" + assertNoLeakedSecrets: false + deploymentStrategy: + type: Recreate + persistence: + enabled: false + initChownData: + enabled: false + plugins: + - grafana-llm-app + - grafana-resourcesexporter-app + - grafana-pyroscope-app + - grafana-exploretraces-app + env: + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin" + GF_DIAGNOSTICS_PROFILING_ENABLED: "true" + GF_DIAGNOSTICS_PROFILING_ADDR: "0.0.0.0" + GF_DIAGNOSTICS_PROFILING_PORT: "9094" + sidecar: + enableUniqueFilenames: true + datasources: + enabled: true + dashboards: + enabled: true + folderAnnotation: "k8s-sidecar-target-directory" + annotations: + k8s-sidecar-target-directory: /tmp/dashboards/Kube Prometheus Stack + provider: + foldersFromFilesStructure: true + # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ + grafana.ini: + analytics: + reporting_enabled: false + check_for_updates: false + check_for_plugin_updates: false + security: + disable_gravatar: true + cookie_secure: true + cookie_samesite: lax + strict_transport_security: true + strict_transport_security_preload: true + strict_transport_security_subdomains: true + content_security_policy: true + auth: + disable_login_form: false + users: + allow_sign_up: true + auto_assign_org: true + server: + root_url: "http://localhost:9090" +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: kube-prometheus-stack + namespace: flux-system +spec: + interval: 24h0m0s + url: https://prometheus-community.github.io/helm-charts diff --git a/hack/distro/monitoring/kustomization.yaml b/hack/distro/monitoring/kustomization.yaml new file mode 100644 index 00000000..52ba0044 --- /dev/null +++ b/hack/distro/monitoring/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ns.yaml + - kube-prometheus-stack/ + - pyroscope/ diff --git a/hack/distro/monitoring/ns.yaml b/hack/distro/monitoring/ns.yaml new file mode 100644 index 00000000..50a6d8e6 --- /dev/null +++ b/hack/distro/monitoring/ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring-system diff --git a/hack/distro/monitoring/pyroscope/kustomization.yaml b/hack/distro/monitoring/pyroscope/kustomization.yaml new file mode 100644 index 00000000..7fcbf108 --- /dev/null +++ b/hack/distro/monitoring/pyroscope/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - release.flux.yaml diff --git a/hack/distro/monitoring/pyroscope/release.flux.yaml b/hack/distro/monitoring/pyroscope/release.flux.yaml new file mode 100644 index 00000000..a2a5153b --- /dev/null +++ b/hack/distro/monitoring/pyroscope/release.flux.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: pyroscope + namespace: flux-system +spec: + serviceAccountName: kustomize-controller + interval: 30s + timeout: 10m + targetNamespace: monitoring-system + releaseName: "pyroscope" + chart: + spec: + chart: pyroscope + version: "1.16.0" + sourceRef: + kind: HelmRepository + name: pyroscope + interval: 24h + install: + createNamespace: true + remediation: + retries: -1 + upgrade: + remediation: + remediateLastFailure: true + retries: -1 + driftDetection: + mode: enabled + values: + global: + dnsService: "kube-dns" + dnsNamespace: "kube-system" + clusterLabelOverride: "kind" + pyroscope: + persistence: + size: 10Gi + podAnnotations: + profiles.grafana.com/memory.scrape: "false" + profiles.grafana.com/goroutine.scrape: "false" + profiles.grafana.com/cpu.scrape: "false" + alloy: + controller: + podAnnotations: + profiles.grafana.com/memory.scrape: "false" + profiles.grafana.com/goroutine.scrape: "false" + profiles.grafana.com/cpu.scrape: "false" +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: pyroscope + namespace: flux-system +spec: + interval: 24h0m0s + url: https://grafana.github.io/helm-charts diff --git a/internal/controllers/tenant/limitranges.go b/internal/controllers/tenant/limitranges.go index e727afad..46d49e14 100644 --- a/internal/controllers/tenant/limitranges.go +++ b/internal/controllers/tenant/limitranges.go @@ -21,8 +21,9 @@ import ( // Ensuring all the LimitRange are applied to each Namespace handled by the Tenant. func (r *Manager) syncLimitRanges(ctx context.Context, tenant *capsulev1beta2.Tenant) error { //nolint:dupl // getting requested LimitRange keys - keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items)) + keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items)) //nolint:staticcheck + //nolint:staticcheck for i := range tenant.Spec.LimitRanges.Items { keys = append(keys, strconv.Itoa(i)) } @@ -45,6 +46,7 @@ func (r *Manager) syncLimitRange(ctx context.Context, tenant *capsulev1beta2.Ten return err } + //nolint:staticcheck for i, spec := range tenant.Spec.LimitRanges.Items { //nolint:dupl target := &corev1.LimitRange{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controllers/tenant/manager.go b/internal/controllers/tenant/manager.go index 1f1e3e9c..c6f8f852 100644 --- a/internal/controllers/tenant/manager.go +++ b/internal/controllers/tenant/manager.go @@ -285,14 +285,16 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct return result, err } + // Ensuring LimitRange resources - r.Log.V(4).Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items)) + r.Log.V(4).Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items)) //nolint:staticcheck if err = r.syncLimitRanges(ctx, instance); err != nil { err = fmt.Errorf("cannot sync limitrange items: %w", err) return result, err } + // Ensuring ResourceQuota resources r.Log.V(4).Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items)) @@ -301,6 +303,7 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct return result, err } + // Ensuring RoleBinding resources r.Log.V(4).Info("Ensuring RoleBindings for Owners and Tenant") diff --git a/internal/controllers/tenant/networkpolicies.go b/internal/controllers/tenant/networkpolicies.go index b937d57f..e4ab6686 100644 --- a/internal/controllers/tenant/networkpolicies.go +++ b/internal/controllers/tenant/networkpolicies.go @@ -21,8 +21,9 @@ import ( // Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant. func (r *Manager) syncNetworkPolicies(ctx context.Context, tenant *capsulev1beta2.Tenant) error { //nolint:dupl // getting requested NetworkPolicy keys - keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items)) + keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items)) //nolint:staticcheck + //nolint:staticcheck for i := range tenant.Spec.NetworkPolicies.Items { keys = append(keys, strconv.Itoa(i)) } @@ -45,6 +46,7 @@ func (r *Manager) syncNetworkPolicy(ctx context.Context, tenant *capsulev1beta2. return err } + //nolint:staticcheck for i, spec := range tenant.Spec.NetworkPolicies.Items { //nolint:dupl target := &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/webhook/cfg/warnings.go b/internal/webhook/cfg/warnings.go new file mode 100644 index 00000000..4394413f --- /dev/null +++ b/internal/webhook/cfg/warnings.go @@ -0,0 +1,71 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package cfg + +import ( + "context" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + capsulewebhook "github.com/projectcapsule/capsule/internal/webhook" + "github.com/projectcapsule/capsule/internal/webhook/utils" +) + +type warningHandler struct{} + +func WarningHandler() capsulewebhook.Handler { + return &warningHandler{} +} + +func (h *warningHandler) OnCreate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(_ context.Context, req admission.Request) *admission.Response { + return h.handle(decoder, req) + } +} + +func (h *warningHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *warningHandler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(_ context.Context, req admission.Request) *admission.Response { + return h.handle(decoder, req) + } +} + +func (h *warningHandler) handle(decoder admission.Decoder, req admission.Request) *admission.Response { + config := &capsulev1beta2.CapsuleConfiguration{} + if err := decoder.Decode(req, config); err != nil { + return utils.ErroredResponse(err) + } + + response := &admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + UID: req.UID, + Allowed: true, + }, + } + + //nolint:staticcheck + if len(config.Spec.UserGroups) > 0 { + response.Warnings = append(response.Warnings, + "The field `userGroups` is deprecated and will be removed in a future release. Please migrate to the `users` field. See: https://projectcapsule.dev/docs/operating/setup/configuration/#users.", + ) + } + + //nolint:staticcheck + if len(config.Spec.UserNames) > 0 { + response.Warnings = append(response.Warnings, + "The field `userNames` is deprecated and will be removed in a future release. Please migrate to the `users` field. See: https://projectcapsule.dev/docs/operating/setup/configuration/#users.", + ) + } + + return response +} diff --git a/internal/webhook/ingress/errors.go b/internal/webhook/ingress/errors.go index f48f4dd4..29f2f95c 100644 --- a/internal/webhook/ingress/errors.go +++ b/internal/webhook/ingress/errors.go @@ -108,6 +108,7 @@ func appendHostnameError(spec api.AllowedListSpec) (append string) { append = fmt.Sprintf(", specify one of the following (%s)", strings.Join(spec.Exact, ", ")) } + //nolint:staticcheck if len(spec.Regex) > 0 { append += fmt.Sprintf(", or matching the regex %s", spec.Regex) } diff --git a/internal/webhook/ingress/validate_hostnames.go b/internal/webhook/ingress/validate_hostnames.go index fd73b3cd..acab8474 100644 --- a/internal/webhook/ingress/validate_hostnames.go +++ b/internal/webhook/ingress/validate_hostnames.go @@ -114,6 +114,7 @@ func (r *hostnames) validateHostnames(tenant capsulev1beta2.Tenant, hostnames se var notMatchingHostnames []string + //nolint:staticcheck if allowedRegex := tenant.Spec.IngressOptions.AllowedHostnames.Regex; len(allowedRegex) > 0 { for currentHostname := range hostnames { matched, _ = regexp.MatchString(allowedRegex, currentHostname) diff --git a/internal/webhook/pod/containerregistry_errors.go b/internal/webhook/pod/containerregistry_errors.go index fca8d99c..884d1497 100644 --- a/internal/webhook/pod/containerregistry_errors.go +++ b/internal/webhook/pod/containerregistry_errors.go @@ -43,6 +43,7 @@ func (f registryClassForbiddenError) Error() (err string) { extra = append(extra, fmt.Sprintf("use one from the following list (%s)", strings.Join(f.spec.Exact, ", "))) } + //nolint:staticcheck if len(f.spec.Regex) > 0 { extra = append(extra, fmt.Sprintf(" use one matching the following regex (%s)", f.spec.Regex)) } diff --git a/internal/webhook/route/config.go b/internal/webhook/route/config.go new file mode 100644 index 00000000..7f202faf --- /dev/null +++ b/internal/webhook/route/config.go @@ -0,0 +1,24 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/projectcapsule/capsule/internal/webhook" +) + +type configValidating struct { + handlers []capsulewebhook.Handler +} + +func ConfigValidation(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &configValidating{handlers: handler} +} + +func (w *configValidating) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *configValidating) GetPath() string { + return "/config/validating" +} diff --git a/internal/webhook/tenant/validation/containerregistry_regex.go b/internal/webhook/tenant/validation/containerregistry_regex.go index dfb67c28..6a133f65 100644 --- a/internal/webhook/tenant/validation/containerregistry_regex.go +++ b/internal/webhook/tenant/validation/containerregistry_regex.go @@ -49,6 +49,7 @@ func (h *containerRegistryRegexHandler) OnUpdate(_ client.Client, decoder admiss } } +//nolint:staticcheck func (h *containerRegistryRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response { tenant := &capsulev1beta2.Tenant{} if err := decoder.Decode(req, tenant); err != nil { diff --git a/internal/webhook/tenant/validation/hostname_regex.go b/internal/webhook/tenant/validation/hostname_regex.go index e93a9a6e..fe5d88e8 100644 --- a/internal/webhook/tenant/validation/hostname_regex.go +++ b/internal/webhook/tenant/validation/hostname_regex.go @@ -49,6 +49,7 @@ func (h *hostnameRegexHandler) OnUpdate(_ client.Client, decoder admission.Decod } } +//nolint:staticcheck func (h *hostnameRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response { tenant := &capsulev1beta2.Tenant{} if err := decoder.Decode(req, tenant); err != nil { diff --git a/internal/webhook/tenant/validation/ingressclass_regex.go b/internal/webhook/tenant/validation/ingressclass_regex.go index ed58de3c..1e478c2f 100644 --- a/internal/webhook/tenant/validation/ingressclass_regex.go +++ b/internal/webhook/tenant/validation/ingressclass_regex.go @@ -49,6 +49,7 @@ func (h *ingressClassRegexHandler) OnUpdate(_ client.Client, decoder admission.D } } +//nolint:staticcheck func (h *ingressClassRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response { tenant := &capsulev1beta2.Tenant{} if err := decoder.Decode(req, tenant); err != nil { diff --git a/internal/webhook/tenant/validation/storageclass_regex.go b/internal/webhook/tenant/validation/storageclass_regex.go index 603fb052..eeb6bd06 100644 --- a/internal/webhook/tenant/validation/storageclass_regex.go +++ b/internal/webhook/tenant/validation/storageclass_regex.go @@ -49,6 +49,7 @@ func (h *storageClassRegexHandler) OnUpdate(_ client.Client, decoder admission.D } } +//nolint:staticcheck func (h *storageClassRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response { tenant := &capsulev1beta2.Tenant{} if err := decoder.Decode(req, tenant); err != nil { diff --git a/internal/webhook/tenant/validation/warnings.go b/internal/webhook/tenant/validation/warnings.go index 9cb805e2..56c8286a 100644 --- a/internal/webhook/tenant/validation/warnings.go +++ b/internal/webhook/tenant/validation/warnings.go @@ -63,32 +63,53 @@ func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, decoder admission.De }, } + //nolint:staticcheck if len(tnt.Spec.LimitRanges.Items) > 0 { - response.Warnings = append(response.Warnings, "Limitranges are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#limitrange-distribution-with-tenantreplications.") + response.Warnings = append(response.Warnings, + "The field `limitRanges` is deprecated and will be removed in a future release. Please migrate to TenantReplications. See: https://projectcapsule.dev/docs/tenants/enforcement/#limitrange-distribution-with-tenantreplications.", + ) } + //nolint:staticcheck if len(tnt.Spec.NetworkPolicies.Items) > 0 { - response.Warnings = append(response.Warnings, "NetworkPolicies are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#networkpolicy-distribution-with-tenantreplications.") + response.Warnings = append(response.Warnings, + "The field `networkPolicies` is deprecated and will be removed in a future release. Please migrate to TenantReplications. See: https://projectcapsule.dev/docs/tenants/enforcement/#networkpolicy-distribution-with-tenantreplications.", + ) } + //nolint:staticcheck if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil { - response.Warnings = append(response.Warnings, "additionalMetadata is deprecated and will be removed int the future. You need to consider to migrate to AdditionalMetadataList: https://projectcapsule.dev/docs/tenants/enforcement/#additionalmetadatalist.") + response.Warnings = append(response.Warnings, + "The field `additionalMetadata` is deprecated and will be removed in a future release. Please migrate to `additionalMetadataList`. See: https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist.", + ) } + //nolint:staticcheck if tnt.Spec.StorageClasses != nil && tnt.Spec.StorageClasses.Regex != "" { - response.Warnings = append(response.Warnings, "Using the regex property to select StorageClasses is deprecated and will be removed int the future.") + response.Warnings = append(response.Warnings, + "The `regex` selector for StorageClasses is deprecated and will be removed in a future release.", + ) } + //nolint:staticcheck if tnt.Spec.GatewayOptions.AllowedClasses != nil && tnt.Spec.GatewayOptions.AllowedClasses.Regex != "" { - response.Warnings = append(response.Warnings, "Using the regex property to select GatewayClasses is deprecated and will be removed int the future.") + response.Warnings = append(response.Warnings, + "The `regex` selector for GatewayClasses is deprecated and will be removed in a future release.", + ) } + //nolint:staticcheck if tnt.Spec.PriorityClasses != nil && tnt.Spec.PriorityClasses.Regex != "" { - response.Warnings = append(response.Warnings, "Using the regex property to select PriorityClasses is deprecated and will be removed int the future.") + response.Warnings = append(response.Warnings, + "The `regex` selector for PriorityClasses is deprecated and will be removed in a future release.", + ) } + //nolint:staticcheck if tnt.Spec.RuntimeClasses != nil && tnt.Spec.RuntimeClasses.Regex != "" { - response.Warnings = append(response.Warnings, "Using the regex property to select RuntimeClasses is deprecated and will be removed int the future.") + response.Warnings = append(response.Warnings, + "The `regex` selector for RuntimeClasses is deprecated and will be removed in a future release.", + ) } return response diff --git a/internal/webhook/utils/error.go b/internal/webhook/utils/error.go index f10f38c1..85111055 100644 --- a/internal/webhook/utils/error.go +++ b/internal/webhook/utils/error.go @@ -29,6 +29,7 @@ func AllowedValuesErrorMessage(allowed api.SelectorAllowedListSpec, err string) extra = append(extra, fmt.Sprintf("use one from the following list (%s)", strings.Join(allowed.Exact, ", "))) } + //nolint:staticcheck if len(allowed.Regex) > 0 { extra = append(extra, fmt.Sprintf("use one matching the following regex (%s)", allowed.Regex)) } diff --git a/pkg/api/allowed_list.go b/pkg/api/allowed_list.go index 02f3fb1a..780b4970 100644 --- a/pkg/api/allowed_list.go +++ b/pkg/api/allowed_list.go @@ -58,7 +58,9 @@ func (in *SelectorAllowedListSpec) SelectorMatch(obj client.Object) bool { type AllowedListSpec struct { // Match exact elements which are allowed as class names within this tenant Exact []string `json:"allowed,omitempty"` - // Match elements by regex (DEPRECATED) + // Deprecated: will be removed in a future release + // + // Match elements by regex. Regex string `json:"allowedRegex,omitempty"` } diff --git a/pkg/api/owner_status_list.go b/pkg/api/owner_status_list.go index 7297aeff..100e7396 100644 --- a/pkg/api/owner_status_list.go +++ b/pkg/api/owner_status_list.go @@ -88,6 +88,7 @@ func (o OwnerStatusListSpec) IsOwner(name string, groups []string) bool { return false } +//nolint:dupl func (o OwnerStatusListSpec) FindOwner(name string, kind OwnerKind) (CoreOwnerSpec, bool) { // Sort in-place by (Kind.String(), Name). sort.Sort(GetByKindAndName(o)) diff --git a/pkg/api/users_list.go b/pkg/api/users_list.go index 4913dd64..edb42118 100644 --- a/pkg/api/users_list.go +++ b/pkg/api/users_list.go @@ -8,21 +8,23 @@ import ( ) // +kubebuilder:object:generate=true - type UserListSpec []UserSpec func (u UserListSpec) IsPresent(name string, groups []string) bool { + groupSet := make(map[string]struct{}, len(groups)) + for _, g := range groups { + groupSet[g] = struct{}{} + } + for _, user := range u { switch user.Kind { case UserOwner, ServiceAccountOwner: - if name == user.Name { + if user.Name == name { return true } case GroupOwner: - for _, group := range groups { - if group == user.Name { - return true - } + if _, ok := groupSet[user.Name]; ok { + return true } } } @@ -30,17 +32,60 @@ func (u UserListSpec) IsPresent(name string, groups []string) bool { return false } -func (o UserListSpec) FindUser(name string, kind OwnerKind) (owner UserSpec) { +//nolint:dupl +func (o UserListSpec) FindUser(name string, kind OwnerKind) (UserSpec, bool) { sort.Sort(ByKindName(o)) - i := sort.Search(len(o), func(i int) bool { - return o[i].Kind >= kind && o[i].Name >= name + + targetKind := kind.String() + n := len(o) + + idx := sort.Search(n, func(i int) bool { + ki := o[i].Kind.String() + + switch { + case ki > targetKind: + return true + case ki < targetKind: + return false + default: + return o[i].Name >= name + } }) - if i < len(o) && o[i].Kind == kind && o[i].Name == name { - return o[i] + if idx < n && + o[idx].Kind.String() == targetKind && + o[idx].Name == name { + return o[idx], true } - return owner + return UserSpec{}, false +} + +func (o UserListSpec) GetByKinds(kinds []OwnerKind) []string { + if len(o) == 0 || len(kinds) == 0 { + return nil + } + + kindSet := make(map[OwnerKind]struct{}, len(kinds)) + for _, k := range kinds { + kindSet[k] = struct{}{} + } + + names := make([]string, 0, len(o)) + + for _, u := range o { + if _, ok := kindSet[u.Kind]; ok { + names = append(names, u.Name) + } + } + + if len(names) == 0 { + return nil + } + + sort.Strings(names) + + return names } type ByKindName UserListSpec diff --git a/pkg/api/users_list_test.go b/pkg/api/users_list_test.go new file mode 100644 index 00000000..70bf0b44 --- /dev/null +++ b/pkg/api/users_list_test.go @@ -0,0 +1,325 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package api_test + +import ( + "math/rand" + "reflect" + "sort" + "testing" + "time" + + "github.com/projectcapsule/capsule/pkg/api" +) + +func linearFindUser(list api.UserListSpec, name string, kind api.OwnerKind) (api.UserSpec, bool) { + for _, u := range list { + if u.Kind == kind && u.Name == name { + return u, true + } + } + return api.UserSpec{}, false +} + +func slowIsPresent(u api.UserListSpec, name string, groups []string) bool { + for _, user := range u { + switch user.Kind { + case api.UserOwner, api.ServiceAccountOwner: + if name == user.Name { + return true + } + case api.GroupOwner: + for _, group := range groups { + if group == user.Name { + return true + } + } + } + } + return false +} + +func TestByKindNameOrdering_UserListSpec(t *testing.T) { + u := api.UserListSpec{ + api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner}, + api.UserSpec{Name: "z", Kind: api.UserOwner}, + api.UserSpec{Name: "a", Kind: api.GroupOwner}, + api.UserSpec{Name: "a", Kind: api.UserOwner}, + } + + // Sort using production ordering + got := append(api.UserListSpec(nil), u...) + sort.Sort(api.ByKindName(got)) + + // Manually sorted expectation using the same logic. + want := append(api.UserListSpec(nil), u...) + sort.Slice(want, func(i, j int) bool { + if want[i].Kind.String() != want[j].Kind.String() { + return want[i].Kind.String() < want[j].Kind.String() + } + return want[i].Name < want[j].Name + }) + + if len(got) != len(want) { + t.Fatalf("length mismatch: got %d, want %d", len(got), len(want)) + } + for i := range got { + if !reflect.DeepEqual(got[i], want[i]) { + t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i]) + } + } +} + +func TestFindUser_Randomized(t *testing.T) { + rnd := rand.New(rand.NewSource(42)) + + ownerKinds := []api.OwnerKind{ + api.GroupOwner, + api.UserOwner, + api.ServiceAccountOwner, + } + + const ( + numLists = 200 + maxLength = 40 + numLookupsPerList = 80 + ) + + for listIdx := 0; listIdx < numLists; listIdx++ { + var list api.UserListSpec + n := rnd.Intn(maxLength) + for i := 0; i < n; i++ { + k := ownerKinds[rnd.Intn(len(ownerKinds))] + list = append(list, api.UserSpec{ + Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6 + Kind: k, + }) + } + + for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ { + var qName string + var qKind api.OwnerKind + + if len(list) > 0 && rnd.Float64() < 0.6 { + // 60% of lookups: pick a real element, must be found + pick := list[rnd.Intn(len(list))] + qName = pick.Name + qKind = pick.Kind + } else { + // 40%: random query, may or may not exist + qName = randomName(rnd, 3+rnd.Intn(4)) + qKind = ownerKinds[rnd.Intn(len(ownerKinds))] + } + + listCopy := append(api.UserListSpec(nil), list...) // FindUser sorts in-place + gotUser, gotFound := listCopy.FindUser(qName, qKind) + wantUser, wantFound := linearFindUser(list, qName, qKind) + + if gotFound != wantFound { + t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v", + listIdx, lookupIdx, qName, qKind, gotFound, wantFound) + } + if gotFound && !reflect.DeepEqual(gotUser, wantUser) { + t.Fatalf("list=%d lookup=%d: user mismatch for (%q,%v):\n got= %+v\nwant= %+v", + listIdx, lookupIdx, qName, qKind, gotUser, wantUser) + } + } + } +} + +func TestIsPresent_RandomizedMatchesSlowImplementation(t *testing.T) { + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + + ownerKinds := []api.OwnerKind{ + api.UserOwner, + api.GroupOwner, + api.ServiceAccountOwner, + } + + const ( + numLists = 200 + maxOwnersPerList = 30 + numLookupsPerList = 80 + maxGroupsPerUser = 10 + ) + + for listIdx := 0; listIdx < numLists; listIdx++ { + // Generate a random user list (possibly with duplicates). + var users api.UserListSpec + nOwners := rnd.Intn(maxOwnersPerList) + for i := 0; i < nOwners; i++ { + kind := ownerKinds[rnd.Intn(len(ownerKinds))] + users = append(users, api.UserSpec{ + Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6 + Kind: kind, + }) + } + + for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ { + // Generate a random userName and groups, + // sometimes biased to hit existing owners/groups. + var userName string + var groups []string + + // 50% of the time: pick an existing owner name as userName + if len(users) > 0 && rnd.Float64() < 0.5 { + pick := users[rnd.Intn(len(users))] + userName = pick.Name + } else { + userName = randomName(rnd, 3+rnd.Intn(4)) + } + + // Random groups, sometimes including owner names + nGroups := rnd.Intn(maxGroupsPerUser) + for i := 0; i < nGroups; i++ { + if len(users) > 0 && rnd.Float64() < 0.5 { + pick := users[rnd.Intn(len(users))] + groups = append(groups, pick.Name) + } else { + groups = append(groups, randomName(rnd, 3+rnd.Intn(4))) + } + } + + got := users.IsPresent(userName, groups) + want := slowIsPresent(users, userName, groups) + + if got != want { + t.Fatalf("list=%d lookup=%d: mismatch\n users=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v", + listIdx, lookupIdx, users, userName, groups, got, want) + } + } + } +} + +func TestGetByKinds_Basic(t *testing.T) { + users := api.UserListSpec{ + api.UserSpec{Name: "alice", Kind: api.UserOwner}, + api.UserSpec{Name: "svc-1", Kind: api.ServiceAccountOwner}, + api.UserSpec{Name: "team-a", Kind: api.GroupOwner}, + api.UserSpec{Name: "bob", Kind: api.UserOwner}, + api.UserSpec{Name: "team-b", Kind: api.GroupOwner}, + } + + eqStrings := func(got, want []string) bool { + if got == nil && want == nil { + return true + } + if len(got) != len(want) { + return false + } + sort.Strings(got) + sort.Strings(want) + for i := range got { + if got[i] != want[i] { + return false + } + } + return true + } + + // Single kind: UserOwner + gotUsers := users.GetByKinds([]api.OwnerKind{api.UserOwner}) + wantUsers := []string{"alice", "bob"} + if !eqStrings(gotUsers, wantUsers) { + t.Fatalf("GetByKinds([UserOwner]) = %v, want %v", gotUsers, wantUsers) + } + + // Single kind: GroupOwner + gotGroups := users.GetByKinds([]api.OwnerKind{api.GroupOwner}) + wantGroups := []string{"team-a", "team-b"} + if !eqStrings(gotGroups, wantGroups) { + t.Fatalf("GetByKinds([GroupOwner]) = %v, want %v", gotGroups, wantGroups) + } + + // Multiple kinds: UserOwner + ServiceAccountOwner + gotUsersAndSAs := users.GetByKinds([]api.OwnerKind{api.UserOwner, api.ServiceAccountOwner}) + wantUsersAndSAs := []string{"alice", "bob", "svc-1"} + if !eqStrings(gotUsersAndSAs, wantUsersAndSAs) { + t.Fatalf("GetByKinds([UserOwner,ServiceAccountOwner]) = %v, want %v", + gotUsersAndSAs, wantUsersAndSAs) + } + + // No kinds → nil + gotNone := users.GetByKinds(nil) + if gotNone != nil { + t.Fatalf("GetByKinds(nil) = %v, want nil", gotNone) + } + + // Kind not present at all + gotUnknown := users.GetByKinds([]api.OwnerKind{api.OwnerKind("does-not-exist")}) + if gotUnknown != nil { + t.Fatalf("GetByKinds([unknown]) = %v, want nil", gotUnknown) + } +} + +func TestGetByKinds_Randomized(t *testing.T) { + rnd := rand.New(rand.NewSource(123)) + + ownerKinds := []api.OwnerKind{ + api.UserOwner, + api.GroupOwner, + api.ServiceAccountOwner, + } + + const ( + numLists = 200 + maxOwnersPerList = 50 + ) + + for listIdx := 0; listIdx < numLists; listIdx++ { + var users api.UserListSpec + n := rnd.Intn(maxOwnersPerList) + for i := 0; i < n; i++ { + k := ownerKinds[rnd.Intn(len(ownerKinds))] + users = append(users, api.UserSpec{ + Name: randomName(rnd, 3+rnd.Intn(4)), // reuse your helper + Kind: k, + }) + } + + // Try several random kind-subsets per list + for subsetIdx := 0; subsetIdx < 10; subsetIdx++ { + // Build a random subset of kinds + var kinds []api.OwnerKind + for _, k := range ownerKinds { + if rnd.Float64() < 0.5 { + kinds = append(kinds, k) + } + } + + got := users.GetByKinds(kinds) + + // Reference implementation: filter + sort + kindSet := make(map[api.OwnerKind]struct{}, len(kinds)) + for _, k := range kinds { + kindSet[k] = struct{}{} + } + + var want []string + if len(kinds) > 0 { + for _, u := range users { + if _, ok := kindSet[u.Kind]; ok { + want = append(want, u.Name) + } + } + } + + if len(want) == 0 { + if got != nil { + t.Fatalf("list=%d subset=%d: expected nil, got %v (kinds=%v, users=%v)", + listIdx, subsetIdx, got, kinds, users) + } + continue + } + + sort.Strings(want) + sort.Strings(got) + + if !reflect.DeepEqual(got, want) { + t.Fatalf("list=%d subset=%d: GetByKinds mismatch\n kinds=%v\n got= %v\n want= %v", + listIdx, subsetIdx, kinds, got, want) + } + } + } +} diff --git a/pkg/configuration/client.go b/pkg/configuration/client.go index 53e2519e..39949b1e 100644 --- a/pkg/configuration/client.go +++ b/pkg/configuration/client.go @@ -30,7 +30,7 @@ func NewCapsuleConfiguration(ctx context.Context, client client.Client, name str if apierrors.IsNotFound(err) { return &capsulev1beta2.CapsuleConfiguration{ Spec: capsulev1beta2.CapsuleConfigurationSpec{ - UserGroups: []string{"projectcapsule.dev"}, + Users: []capsuleapi.UserSpec{{Name: "projectcapsule.dev", Kind: capsuleapi.GroupOwner}}, ForceTenantPrefix: false, ProtectedNamespaceRegexpString: "", }, @@ -86,12 +86,14 @@ func (c *capsuleConfiguration) ValidatingWebhookConfigurationName() (name string return c.retrievalFn().Spec.CapsuleResources.ValidatingWebhookConfigurationName } +//nolint:staticcheck func (c *capsuleConfiguration) UserGroups() []string { - return c.retrievalFn().Spec.UserGroups + return append(c.retrievalFn().Spec.UserGroups, c.retrievalFn().Spec.Users.GetByKinds([]capsuleapi.OwnerKind{capsuleapi.GroupOwner})...) } +//nolint:staticcheck func (c *capsuleConfiguration) UserNames() []string { - return c.retrievalFn().Spec.UserNames + return append(c.retrievalFn().Spec.UserNames, c.retrievalFn().Spec.Users.GetByKinds([]capsuleapi.OwnerKind{capsuleapi.UserOwner, capsuleapi.ServiceAccountOwner})...) } func (c *capsuleConfiguration) IgnoreUserWithGroups() []string { diff --git a/pkg/utils/tenant/metdata.go b/pkg/utils/tenant/metdata.go index 833302e7..25dac3e3 100644 --- a/pkg/utils/tenant/metdata.go +++ b/pkg/utils/tenant/metdata.go @@ -73,6 +73,7 @@ func BuildNamespaceMetadataForTenant(ns *corev1.Namespace, tnt *capsulev1beta2.T func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string { annotations := make(map[string]string) + //nolint:staticcheck if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil { maps.Copy(annotations, md.AdditionalMetadata.Annotations) } @@ -86,6 +87,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s annotations[meta.AvailableIngressClassesAnnotation] = strings.Join(ic.Exact, ",") } + //nolint:staticcheck if len(ic.Regex) > 0 { annotations[meta.AvailableIngressClassesRegexpAnnotation] = ic.Regex } @@ -96,6 +98,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s annotations[meta.AvailableStorageClassesAnnotation] = strings.Join(sc.Exact, ",") } + //nolint:staticcheck if len(sc.Regex) > 0 { annotations[meta.AvailableStorageClassesRegexpAnnotation] = sc.Regex } @@ -106,6 +109,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s annotations[meta.AllowedRegistriesAnnotation] = strings.Join(cr.Exact, ",") } + //nolint:staticcheck if len(cr.Regex) > 0 { annotations[meta.AllowedRegistriesRegexpAnnotation] = cr.Regex } @@ -128,6 +132,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s func BuildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string { labels := make(map[string]string) + //nolint:staticcheck if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil { maps.Copy(labels, md.AdditionalMetadata.Labels) }