From e96b1f98e929fb9f07e6609c6a358b28a2125910 Mon Sep 17 00:00:00 2001 From: davidgrldo Date: Fri, 11 Sep 2026 23:36:23 +0700 Subject: [PATCH 1/2] feat(uptimekuma): add Uptime Kuma monitor provider Add support for Uptime Kuma (2.x) as an uptime monitor provider, managed through its socket.io API via github.com/breml/go-uptime-kuma-client: - New UpTimeKumaMonitorService implementing the MonitorService interface with a single lazily (re)connected client, guarded by a mutex - New uptimeKumaConfig in the EndpointMonitor CRD: interval (min 20), monitorType (http|keyword), keywordValue, keywordExists, notifications - Auth via username/password reusing the existing provider config fields (Uptime Kuma API keys are not accepted for monitor management) - Update fetches the full monitor object and overrides controller-managed fields, matching Uptime Kuma's editMonitor semantics - Docs: docs/uptimekuma-configuration.md, README provider list, sample test config in CONTRIBUTING Dependency note: github.com/breml/go-uptime-kuma-client requires go >= 1.25.2 (go directive bumped; CI reads the Go version from go.mod). The remaining go.mod churn is MVS-forced by the new module's requirements, verified minimal. Fixes #627 Signed-off-by: davidgrldo --- CONTRIBUTING.md | 4 + README.md | 1 + api/v1alpha1/endpointmonitor_types.go | 30 ++ api/v1alpha1/zz_generated.deepcopy.go | 20 + ...monitor.stakater.com_endpointmonitors.yaml | 30 ++ ...monitor.stakater.com_endpointmonitors.yaml | 30 ++ ...monitor.stakater.com_endpointmonitors.yaml | 30 ++ docs/uptimekuma-configuration.md | 53 +++ go.mod | 85 ++-- go.sum | 257 +++++++----- .../controller/endpointmonitor_controller.go | 3 + pkg/config/config.go | 6 + pkg/monitors/monitor-proxy.go | 6 + pkg/monitors/uptimekuma/uptimekuma-monitor.go | 372 ++++++++++++++++++ .../uptimekuma/uptimekuma-monitor_test.go | 325 +++++++++++++++ 15 files changed, 1115 insertions(+), 137 deletions(-) create mode 100644 docs/uptimekuma-configuration.md create mode 100644 pkg/monitors/uptimekuma/uptimekuma-monitor.go create mode 100644 pkg/monitors/uptimekuma/uptimekuma-monitor_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a3d7cee..6d5f6ab6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -179,6 +179,10 @@ providers: apiKey: apiURL: https://api.uptimerobot.com/v2/ alertContacts: + - name: UptimeKuma + apiURL: http://uptime-kuma.uptime-kuma.svc:3001 + username: + password: - name: StatusCake apiKey: apiURL: https://app.statuscake.com/API/ diff --git a/README.md b/README.md index bba26d95..cfdf28cf 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Currently we support the following monitors: - [Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/monitor-web-app-availability) ([Additional Config](docs/appinsights-configuration.md)) - [gcloud](https://cloud.google.com/monitoring/uptime-checks) ([Additional Config](docs/gcloud-configuration.md)) - [Grafana](https://grafana.com/grafana/plugins/grafana-synthetic-monitoring-app/) ([Additional Config](docs/grafana-configuration.md)) +- [Uptime Kuma](https://uptime.kuma.pet) ([Additional Config](docs/uptimekuma-configuration.md)) ## Usage diff --git a/api/v1alpha1/endpointmonitor_types.go b/api/v1alpha1/endpointmonitor_types.go index 065c33d1..80c5c425 100644 --- a/api/v1alpha1/endpointmonitor_types.go +++ b/api/v1alpha1/endpointmonitor_types.go @@ -75,6 +75,10 @@ type EndpointMonitorSpec struct { // Configuration for Grafana Cloud Monitor Provider // +optional GrafanaConfig *GrafanaConfig `json:"grafanaConfig,omitempty"` + + // Configuration for Uptime Kuma Monitor Provider + // +optional + UptimeKumaConfig *UptimeKumaConfig `json:"uptimeKumaConfig,omitempty"` } // UptimeRobotConfig defines the configuration for UptimeRobot Monitor Provider @@ -430,6 +434,32 @@ type GrafanaConfig struct { AlertSensitivity string `json:"alertSensitivity,omitempty"` } +// UptimeKumaConfig defines the configuration for UptimeKuma Monitor Provider +type UptimeKumaConfig struct { + // The uptime kuma check interval in seconds (Uptime Kuma minimum is 20) + // +kubebuilder:validation:Minimum=20 + // +optional + Interval int `json:"interval,omitempty"` + + // The uptime kuma monitor type (http or keyword) + // +kubebuilder:validation:Enum=http;keyword + // +optional + MonitorType string `json:"monitorType,omitempty"` + + // keyword to check on URL (Only if monitor-type is keyword) + // +optional + KeywordValue string `json:"keywordValue,omitempty"` + + // Alert if value exist (yes) or doesn't exist (no) (Only if monitor-type is keyword) + // +kubebuilder:validation:Enum=yes;no + // +optional + KeywordExists string `json:"keywordExists,omitempty"` + + // Comma-separated uptime kuma notification IDs to attach to this monitor + // +optional + Notifications string `json:"notifications,omitempty"` +} + // URLSource represents the set of resources to fetch the URL from type URLSource struct { // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 87f4f2ad..e0bb046b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -151,6 +151,11 @@ func (in *EndpointMonitorSpec) DeepCopyInto(out *EndpointMonitorSpec) { *out = new(GrafanaConfig) (*in).DeepCopyInto(*out) } + if in.UptimeKumaConfig != nil { + in, out := &in.UptimeKumaConfig, &out.UptimeKumaConfig + *out = new(UptimeKumaConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointMonitorSpec. @@ -377,6 +382,21 @@ func (in *UptimeConfig) DeepCopy() *UptimeConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UptimeKumaConfig) DeepCopyInto(out *UptimeKumaConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UptimeKumaConfig. +func (in *UptimeKumaConfig) DeepCopy() *UptimeKumaConfig { + if in == nil { + return nil + } + out := new(UptimeKumaConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UptimeRobotConfig) DeepCopyInto(out *UptimeRobotConfig) { *out = *in diff --git a/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml b/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml index 08ba4aa0..21bcf871 100644 --- a/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -373,6 +373,36 @@ spec: description: Add one or more tags for the check separated by `,` type: string type: object + uptimeKumaConfig: + description: Configuration for Uptime Kuma Monitor Provider + properties: + interval: + description: The uptime kuma check interval in seconds (Uptime + Kuma minimum is 20) + minimum: 20 + type: integer + keywordExists: + description: Alert if value exist (yes) or doesn't exist (no) + (Only if monitor-type is keyword) + enum: + - "yes" + - "no" + type: string + keywordValue: + description: keyword to check on URL (Only if monitor-type is + keyword) + type: string + monitorType: + description: The uptime kuma monitor type (http or keyword) + enum: + - http + - keyword + type: string + notifications: + description: Comma-separated uptime kuma notification IDs to attach + to this monitor + type: string + type: object uptimeRobotConfig: description: Configuration for UptimeRobot Monitor Provider properties: diff --git a/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml b/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml index 762e0334..db792b68 100644 --- a/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -373,6 +373,36 @@ spec: description: Add one or more tags for the check separated by `,` type: string type: object + uptimeKumaConfig: + description: Configuration for Uptime Kuma Monitor Provider + properties: + interval: + description: The uptime kuma check interval in seconds (Uptime + Kuma minimum is 20) + minimum: 20 + type: integer + keywordExists: + description: Alert if value exist (yes) or doesn't exist (no) + (Only if monitor-type is keyword) + enum: + - "yes" + - "no" + type: string + keywordValue: + description: keyword to check on URL (Only if monitor-type is + keyword) + type: string + monitorType: + description: The uptime kuma monitor type (http or keyword) + enum: + - http + - keyword + type: string + notifications: + description: Comma-separated uptime kuma notification IDs to attach + to this monitor + type: string + type: object uptimeRobotConfig: description: Configuration for UptimeRobot Monitor Provider properties: diff --git a/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml b/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml index 762e0334..db792b68 100644 --- a/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -373,6 +373,36 @@ spec: description: Add one or more tags for the check separated by `,` type: string type: object + uptimeKumaConfig: + description: Configuration for Uptime Kuma Monitor Provider + properties: + interval: + description: The uptime kuma check interval in seconds (Uptime + Kuma minimum is 20) + minimum: 20 + type: integer + keywordExists: + description: Alert if value exist (yes) or doesn't exist (no) + (Only if monitor-type is keyword) + enum: + - "yes" + - "no" + type: string + keywordValue: + description: keyword to check on URL (Only if monitor-type is + keyword) + type: string + monitorType: + description: The uptime kuma monitor type (http or keyword) + enum: + - http + - keyword + type: string + notifications: + description: Comma-separated uptime kuma notification IDs to attach + to this monitor + type: string + type: object uptimeRobotConfig: description: Configuration for UptimeRobot Monitor Provider properties: diff --git a/docs/uptimekuma-configuration.md b/docs/uptimekuma-configuration.md new file mode 100644 index 00000000..1fc1cd3b --- /dev/null +++ b/docs/uptimekuma-configuration.md @@ -0,0 +1,53 @@ +# Uptime Kuma Configuration + +Uptime Kuma support speaks the Uptime Kuma **2.x** socket.io API via [go-uptime-kuma-client](https://github.com/breml/go-uptime-kuma-client). Older Uptime Kuma versions (1.x) are not supported. + +Notes: + +- Uptime Kuma API keys do **not** work for creating, updating or deleting monitors, so the controller authenticates with **username and password**. +- **2FA must be disabled** for the account the controller uses — create a dedicated service account in Uptime Kuma without two-factor authentication. +- Point `apiURL` at the internal HTTP service (e.g. `http://uptime-kuma.uptime-kuma.svc:3001`) rather than an ingress to avoid self-signed certificate issues. + +## Configuration + +Add a provider entry to the controller's config: + +```yaml +providers: + - name: UptimeKuma + apiURL: http://uptime-kuma.uptime-kuma.svc:3001 + username: + password: +``` + +Additional uptime kuma configurations can be added to the EndpointMonitor through these fields: + +| Fields | Description | +|:--------------:|:--------------------------------------------------------------------------------------------------------:| +| Interval | The uptime kuma check interval in seconds (minimum 20, defaults to 60) | +| MonitorType | The uptime kuma monitor type (http or keyword) | +| KeywordExists | Alert if value exist (yes) or doesn't exist (no) (Only if monitor-type is keyword) | +| KeywordValue | keyword to check on URL (e.g.'search' or '404') (Only if monitor-type is keyword) | +| Notifications | Comma-separated uptime kuma notification IDs to attach to this monitor | + +### Fetching notification IDs from Uptime Kuma + +Notification IDs are the identifiers of notification targets (e.g. a Slack or ntfy integration) configured in Uptime Kuma. You can find the ID of a notification in **Settings > Notifications** — it is shown in the URL when editing a notification, or in the notification list API. + +## Example + +```yaml +apiVersion: endpointmonitor.stakater.com/v1alpha1 +kind: EndpointMonitor +metadata: + name: stakater +spec: + forceHttps: true + url: https://stakater.com/ + uptimeKumaConfig: + interval: 120 + monitorType: keyword + keywordExists: no + keywordValue: 404 + notifications: "1,2" +``` diff --git a/go.mod b/go.mod index 3b9b27bb..d4c701eb 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,18 @@ module github.com/stakater/IngressMonitorController/v2 -go 1.24.0 +go 1.25.2 require ( - cloud.google.com/go/monitoring v1.16.3 + cloud.google.com/go/monitoring v1.24.2 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 github.com/StatusCakeDev/statuscake-go v1.3.0 github.com/antoineaugusti/updown v0.0.0-20190412074625-d590ab97f115 - github.com/go-logr/logr v1.4.2 - github.com/google/go-cmp v0.6.0 + github.com/breml/go-uptime-kuma-client v0.4.2 + github.com/go-logr/logr v1.4.3 + github.com/google/go-cmp v0.7.0 github.com/grafana/synthetic-monitoring-agent v0.19.4 github.com/grafana/synthetic-monitoring-api-go-client v0.8.0 github.com/karlderkaefer/pingdom-golang-client v1.0.4 @@ -19,10 +20,10 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/russellcardullo/go-pingdom v1.3.0 github.com/stakater/operator-utils v0.1.13 - github.com/stretchr/testify v1.10.0 - golang.org/x/time v0.3.0 - google.golang.org/api v0.149.0 - google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 + github.com/stretchr/testify v1.11.1 + golang.org/x/time v0.12.0 + google.golang.org/api v0.239.0 + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 gopkg.in/yaml.v2 v2.4.0 gotest.tools v2.2.0+incompatible k8s.io/api v0.31.0 @@ -32,7 +33,9 @@ require ( ) require ( - cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect @@ -46,7 +49,7 @@ require ( github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect @@ -54,16 +57,16 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.20.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -71,44 +74,48 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/maldikhan/go.socket.io v0.1.1 // indirect + github.com/maniartech/signals v1.3.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect - google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 938e84a7..7897686a 100644 --- a/go.sum +++ b/go.sum @@ -9,22 +9,28 @@ cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6T cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= -cloud.google.com/go/monitoring v1.16.3/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= @@ -43,6 +49,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= @@ -58,7 +66,11 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -88,6 +100,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/breml/go-uptime-kuma-client v0.4.2 h1:9n630BMjethzqh4O5cpps0ilrQYukSV59fln2rEJiYQ= +github.com/breml/go-uptime-kuma-client v0.4.2/go.mod h1:rCvWTrKsbBPw79rFFtq5xLZNG48Ejc6iKeAddhtz2jo= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -99,8 +113,11 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -111,7 +128,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -122,6 +139,14 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= +github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -131,10 +156,13 @@ github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -148,8 +176,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -165,8 +193,8 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU= @@ -193,26 +221,30 @@ github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -241,11 +273,10 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -259,19 +290,21 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= @@ -342,6 +375,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -364,6 +399,10 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maldikhan/go.socket.io v0.1.1 h1:4yNNZRwbdcpw6NyXSXyLiQOMzNEXJyKsUZKaMihkRi0= +github.com/maldikhan/go.socket.io v0.1.1/go.mod h1:H1EoWDJvqfV2WryM8F9og6ZkH7NsZDlHr0BRkKb58tY= +github.com/maniartech/signals v1.3.1 h1:pT3dK6x5Un+B6L3ZLAKygEe+L49TClPreyT08vOoHXY= +github.com/maniartech/signals v1.3.1/go.mod h1:AbE8Yy9ZjKCWNU/VhQ+0Ea9KOaTWHp6aOfdLBe5m1iM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -379,7 +418,13 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -410,9 +455,17 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/openshift/api v0.0.0-20200526144822-34f54f12813a h1:riE/kCXnb051RWT/z+DytxKEZ3+JromVDl79rXAKyFY= github.com/openshift/api v0.0.0-20200526144822-34f54f12813a/go.mod h1:l6TGeqJ92DrZBuWMNKcot1iZUHfbYSJyBWHGgg6Dn6s= github.com/openshift/build-machinery-go v0.0.0-20200424080330-082bf86082cc/go.mod h1:1CkcsT3aVebzRBzVTSbiKSkJMsC/CASqxesfqEMfJEc= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= @@ -424,6 +477,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -433,34 +488,34 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/russellcardullo/go-pingdom v1.3.0 h1:uf5ZJOOR6BxqLcz0Lwmtb6HMFgX20CCLIhy0wB6tc/4= github.com/russellcardullo/go-pingdom v1.3.0/go.mod h1:y9hd/6P97iuRFi6VPGgLxsRQaJQll298jZ/qPQPfcXw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -471,6 +526,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -480,14 +537,16 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stakater/operator-utils v0.1.13 h1:BWOehMy15gv+2qMe6GSEcX4k41C9DyDTbeQq51V63NU= github.com/stakater/operator-utils v0.1.13/go.mod h1:NM2WTCf9xlEQEZVcncTVaVJ3WrvarQbMe8gxZd3Ybpo= @@ -506,14 +565,20 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -525,22 +590,26 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -557,8 +626,12 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -568,10 +641,8 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -635,17 +706,15 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -653,10 +722,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -696,14 +763,10 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -711,17 +774,15 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -768,8 +829,8 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -777,6 +838,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -787,8 +850,8 @@ google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= -google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= +google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -815,23 +878,21 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= -google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -842,8 +903,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/controller/endpointmonitor_controller.go b/internal/controller/endpointmonitor_controller.go index 168cc5eb..7dc60ef9 100644 --- a/internal/controller/endpointmonitor_controller.go +++ b/internal/controller/endpointmonitor_controller.go @@ -148,6 +148,9 @@ func (r *EndpointMonitorReconciler) GetMonitorOfType(spec endpointmonitorv1alpha if spec.GrafanaConfig != nil { return r.GetMonitorServiceOfType(monitors.TypeGrafana) } + if spec.UptimeKumaConfig != nil { + return r.GetMonitorServiceOfType(monitors.TypeUptimeKuma) + } // If none of the above, return the first monitor service return r.MonitorServices[0] } diff --git a/pkg/config/config.go b/pkg/config/config.go index 46a8b4e7..b4958a5a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -146,6 +146,12 @@ func GetControllerConfigTest() Config { configFilePath = "../../../.local/test-config.yaml" } + if _, err := os.Stat(configFilePath); err != nil { + // Without a test config, provider lookups return nil and live-API tests skip themselves + log.Info("Test config file not found, live API tests will be skipped: " + configFilePath) + return Config{} + } + config := ReadConfig(configFilePath) return config diff --git a/pkg/monitors/monitor-proxy.go b/pkg/monitors/monitor-proxy.go index b1e3ba0b..1ac9f22d 100644 --- a/pkg/monitors/monitor-proxy.go +++ b/pkg/monitors/monitor-proxy.go @@ -12,6 +12,7 @@ import ( "github.com/stakater/IngressMonitorController/v2/pkg/monitors/statuscake" "github.com/stakater/IngressMonitorController/v2/pkg/monitors/updown" "github.com/stakater/IngressMonitorController/v2/pkg/monitors/uptime" + "github.com/stakater/IngressMonitorController/v2/pkg/monitors/uptimekuma" "github.com/stakater/IngressMonitorController/v2/pkg/monitors/uptimerobot" logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -28,6 +29,7 @@ const ( TypeAppInsights = "AppInsights" TypeGCloud = "gcloud" TypeGrafana = "Grafana" + TypeUptimeKuma = "UptimeKuma" ) type MonitorServiceProxy struct { @@ -60,6 +62,8 @@ func (mp *MonitorServiceProxy) OfType(mType string) MonitorServiceProxy { mp.monitor = &gcloud.MonitorService{} case TypeGrafana: mp.monitor = &grafana.GrafanaMonitorService{} + case TypeUptimeKuma: + mp.monitor = &uptimekuma.UpTimeKumaMonitorService{} default: panic("No such provider found: " + mType) } @@ -87,6 +91,8 @@ func (mp *MonitorServiceProxy) ExtractConfig(spec endpointmonitorv1alpha1.Endpoi config = spec.GCloudConfig case TypeGrafana: config = spec.GrafanaConfig + case TypeUptimeKuma: + config = spec.UptimeKumaConfig default: return config } diff --git a/pkg/monitors/uptimekuma/uptimekuma-monitor.go b/pkg/monitors/uptimekuma/uptimekuma-monitor.go new file mode 100644 index 00000000..e8f29d82 --- /dev/null +++ b/pkg/monitors/uptimekuma/uptimekuma-monitor.go @@ -0,0 +1,372 @@ +package uptimekuma + +import ( + "context" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + kuma "github.com/breml/go-uptime-kuma-client" + "github.com/breml/go-uptime-kuma-client/monitor" + endpointmonitorv1alpha1 "github.com/stakater/IngressMonitorController/v2/api/v1alpha1" + "github.com/stakater/IngressMonitorController/v2/pkg/config" + "github.com/stakater/IngressMonitorController/v2/pkg/models" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var log = logf.Log.WithName("uptime-kuma-monitor") + +// Default Interval for status checking (Uptime Kuma minimum is 20 seconds) +const DefaultInterval = 60 + +const ( + connectTimeout = 30 * time.Second + requestTimeout = 30 * time.Second +) + +// UpTimeKumaMonitorService manages monitors on an Uptime Kuma server (Kuma 2.x) +// via its socket.io API. One client connection is held for the lifetime of the +// service and lazily re-established after failures. +type UpTimeKumaMonitorService struct { + url string + username string + password string + client *kuma.Client + mutex sync.Mutex +} + +func (service *UpTimeKumaMonitorService) Equal(oldMonitor models.Monitor, newMonitor models.Monitor) bool { + if oldMonitor.URL != newMonitor.URL || !reflect.DeepEqual(normalizeConfig(oldMonitor.Config), normalizeConfig(newMonitor.Config)) { + log.Info(fmt.Sprintf("There are some new changes in %s monitor", newMonitor.Name)) + return false + } + return true +} + +func (service *UpTimeKumaMonitorService) Setup(p config.Provider) { + service.url = p.ApiURL + service.username = p.Username + service.password = p.Password + service.client = nil +} + +// ensureClient returns the connected client, (re)connecting on first use or +// after a previous failure invalidated the connection +func (service *UpTimeKumaMonitorService) ensureClient() (*kuma.Client, error) { + service.mutex.Lock() + defer service.mutex.Unlock() + + if service.client == nil { + ctx, cancel := context.WithTimeout(context.Background(), connectTimeout) + defer cancel() + + client, err := kuma.New(ctx, service.url, service.username, service.password, + kuma.WithConnectTimeout(connectTimeout), + kuma.WithLogLevel(kuma.LogLevelNone), + ) + if err != nil { + return nil, fmt.Errorf("unable to connect to Uptime Kuma at %s: %w", service.url, err) + } + service.client = client + } + + return service.client, nil +} + +// invalidateClient drops the connection so the next call reconnects +func (service *UpTimeKumaMonitorService) invalidateClient() { + service.mutex.Lock() + defer service.mutex.Unlock() + + if service.client != nil { + if err := service.client.Disconnect(); err != nil { + log.Error(err, "Failed to disconnect Uptime Kuma client") + } + service.client = nil + } +} + +func (service *UpTimeKumaMonitorService) GetAll() ([]models.Monitor, error) { + client, err := service.ensureClient() + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + + kumaMonitors, err := client.GetMonitors(ctx) + if err != nil { + service.invalidateClient() + return nil, fmt.Errorf("unable to get monitors from Uptime Kuma: %w", err) + } + + monitors := make([]models.Monitor, 0, len(kumaMonitors)) + for _, kumaMonitor := range kumaMonitors { + monitors = append(monitors, *kumaMonitorToBaseMonitor(kumaMonitor)) + } + + return monitors, nil +} + +func (service *UpTimeKumaMonitorService) GetByName(name string) (*models.Monitor, error) { + monitors, err := service.GetAll() + if err != nil { + return nil, err + } + + // Uptime Kuma has no lookup by name, so match client-side + for _, monitor := range monitors { + if monitor.Name == name { + return &monitor, nil + } + } + + return nil, nil +} + +func (service *UpTimeKumaMonitorService) Add(m models.Monitor) { + normalized := normalizeConfig(m.Config) + + client, err := service.ensureClient() + if err != nil { + log.Error(err, "Monitor couldn't be added: "+m.Name) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + + _, err = client.CreateMonitor(ctx, buildKumaMonitor(m, normalized)) + if err != nil { + service.invalidateClient() + log.Error(err, "Monitor couldn't be added: "+m.Name) + return + } + + log.Info("Monitor Added: " + m.Name) +} + +func (service *UpTimeKumaMonitorService) Update(m models.Monitor) { + id, err := strconv.ParseInt(m.ID, 10, 64) + if err != nil { + log.Error(err, "Monitor couldn't be updated, invalid monitor id: "+m.ID) + return + } + + normalized := normalizeConfig(m.Config) + + client, err := service.ensureClient() + if err != nil { + log.Error(err, "Monitor couldn't be updated: "+m.Name) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + + // Uptime Kuma's editMonitor needs the full object, so fetch the current + // monitor and override the fields managed by the controller + current, err := client.GetMonitor(ctx, id) + if err != nil { + service.invalidateClient() + log.Error(err, "Monitor couldn't be updated: "+m.Name) + return + } + + var updated monitor.Monitor + if normalized.MonitorType == "keyword" { + keywordMonitor := monitor.HTTPKeyword{} + if err := current.As(&keywordMonitor); err != nil { + log.Error(err, "Unable to read current keyword monitor: "+m.Name) + } + keywordMonitor.Name = m.Name + keywordMonitor.URL = m.URL + keywordMonitor.Interval = int64(normalized.Interval) + keywordMonitor.NotificationIDs = parseNotificationIDs(normalized.Notifications) + keywordMonitor.Keyword = normalized.KeywordValue + keywordMonitor.InvertKeyword = keywordInvert(normalized.KeywordExists) + updated = &keywordMonitor + } else { + httpMonitor := monitor.HTTP{} + if err := current.As(&httpMonitor); err != nil { + log.Error(err, "Unable to read current http monitor: "+m.Name) + } + httpMonitor.Name = m.Name + httpMonitor.URL = m.URL + httpMonitor.Interval = int64(normalized.Interval) + httpMonitor.NotificationIDs = parseNotificationIDs(normalized.Notifications) + updated = &httpMonitor + } + + if err := client.UpdateMonitor(ctx, updated); err != nil { + service.invalidateClient() + log.Error(err, "Monitor couldn't be updated: "+m.Name) + return + } + + log.Info("Monitor Updated: " + m.Name) +} + +func (service *UpTimeKumaMonitorService) Remove(m models.Monitor) { + id, err := strconv.ParseInt(m.ID, 10, 64) + if err != nil { + log.Error(err, "Monitor couldn't be removed, invalid monitor id: "+m.ID) + return + } + + client, err := service.ensureClient() + if err != nil { + log.Error(err, "Monitor couldn't be removed: "+m.Name) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + + if err := client.DeleteMonitor(ctx, id); err != nil { + service.invalidateClient() + log.Error(err, "Monitor couldn't be removed: "+m.Name) + return + } + + log.Info("Monitor Removed: " + m.Name) +} + +// buildKumaMonitor builds a typed monitor for creation from the base monitor +// and the normalized provider config +func buildKumaMonitor(m models.Monitor, normalized *endpointmonitorv1alpha1.UptimeKumaConfig) monitor.Monitor { + base := monitor.Base{ + Name: m.Name, + Interval: int64(normalized.Interval), + NotificationIDs: parseNotificationIDs(normalized.Notifications), + IsActive: true, + } + details := monitor.HTTPDetails{ + URL: m.URL, + Method: "GET", + AcceptedStatusCodes: []string{"200-299"}, + } + + if normalized.MonitorType == "keyword" { + if len(normalized.KeywordValue) == 0 { + log.Error(nil, "Monitor is of type Keyword but the `keyword-value` is missing") + } + return &monitor.HTTPKeyword{ + Base: base, + HTTPDetails: details, + HTTPKeywordDetails: monitor.HTTPKeywordDetails{ + Keyword: normalized.KeywordValue, + InvertKeyword: keywordInvert(normalized.KeywordExists), + }, + } + } + + return &monitor.HTTP{ + Base: base, + HTTPDetails: details, + } +} + +// kumaMonitorToBaseMonitor maps a monitor read from Uptime Kuma to the base +// monitor model, rebuilding the provider config for Equal() comparisons +func kumaMonitorToBaseMonitor(kumaMonitor monitor.Base) *models.Monitor { + var m models.Monitor + + m.Name = kumaMonitor.Name + m.ID = strconv.FormatInt(kumaMonitor.ID, 10) + + providerConfig := endpointmonitorv1alpha1.UptimeKumaConfig{ + Interval: int(kumaMonitor.Interval), + MonitorType: kumaMonitor.Type(), + Notifications: joinNotificationIDs(kumaMonitor.NotificationIDs), + } + + if kumaMonitor.Type() == "keyword" { + keywordMonitor := monitor.HTTPKeyword{} + if err := kumaMonitor.As(&keywordMonitor); err == nil { + m.URL = keywordMonitor.URL + providerConfig.KeywordValue = keywordMonitor.Keyword + providerConfig.KeywordExists = invertToKeywordExists(keywordMonitor.InvertKeyword) + } + } else { + httpMonitor := monitor.HTTP{} + if err := kumaMonitor.As(&httpMonitor); err == nil { + m.URL = httpMonitor.URL + } + } + + m.Config = &providerConfig + + return &m +} + +// normalizeConfig applies the provider defaults so monitors read back from +// Uptime Kuma can be compared against monitors built from the CRD +func normalizeConfig(providerConfig interface{}) *endpointmonitorv1alpha1.UptimeKumaConfig { + normalized := &endpointmonitorv1alpha1.UptimeKumaConfig{ + Interval: DefaultInterval, + MonitorType: "http", + KeywordExists: "yes", + } + + if kumaConfig, ok := providerConfig.(*endpointmonitorv1alpha1.UptimeKumaConfig); ok && kumaConfig != nil { + if kumaConfig.Interval > 0 { + normalized.Interval = kumaConfig.Interval + } + if len(kumaConfig.MonitorType) != 0 { + normalized.MonitorType = strings.ToLower(kumaConfig.MonitorType) + } + if len(kumaConfig.KeywordExists) != 0 { + normalized.KeywordExists = strings.ToLower(kumaConfig.KeywordExists) + } + normalized.KeywordValue = kumaConfig.KeywordValue + normalized.Notifications = joinNotificationIDs(parseNotificationIDs(kumaConfig.Notifications)) + } + + return normalized +} + +// parseNotificationIDs parses a comma- or dash-separated list of notification +// IDs into a sorted slice +func parseNotificationIDs(notifications string) []int64 { + if notifications == "" { + return nil + } + + var ids []int64 + for _, part := range strings.FieldsFunc(notifications, func(r rune) bool { return r == ',' || r == '-' }) { + if id, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64); err == nil { + ids = append(ids, id) + } + } + + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + return ids +} + +func joinNotificationIDs(ids []int64) string { + strs := make([]string, 0, len(ids)) + for _, id := range ids { + strs = append(strs, strconv.FormatInt(id, 10)) + } + return strings.Join(strs, ",") +} + +// keywordInvert maps KeywordExists to Uptime Kuma's invertKeyword flag: +// "no" means alert when the keyword does NOT exist +func keywordInvert(keywordExists string) bool { + return strings.EqualFold(keywordExists, "no") +} + +// invertToKeywordExists maps Uptime Kuma's invertKeyword flag back to KeywordExists +func invertToKeywordExists(invertKeyword bool) string { + if invertKeyword { + return "no" + } + return "yes" +} diff --git a/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go b/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go new file mode 100644 index 00000000..5e23703e --- /dev/null +++ b/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go @@ -0,0 +1,325 @@ +package uptimekuma + +import ( + "encoding/json" + "os" + "reflect" + "testing" + + "github.com/breml/go-uptime-kuma-client/monitor" + endpointmonitorv1alpha1 "github.com/stakater/IngressMonitorController/v2/api/v1alpha1" + "github.com/stakater/IngressMonitorController/v2/pkg/config" + "github.com/stakater/IngressMonitorController/v2/pkg/models" + "github.com/stakater/IngressMonitorController/v2/pkg/util" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func init() { + // To allow normal logging to be printed if tests fails + // Dev mode is an extra feature to make output more readable + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) +} + +func TestParseNotificationIDs(t *testing.T) { + cases := []struct { + input string + expected []int64 + }{ + {"", nil}, + {"1", []int64{1}}, + {"1,2,3", []int64{1, 2, 3}}, + {"3-1-2", []int64{1, 2, 3}}, + {"5,1", []int64{1, 5}}, + {" 4 , 2 ", []int64{2, 4}}, + {"1,abc,3", []int64{1, 3}}, + {"abc", nil}, + } + for _, c := range cases { + actual := parseNotificationIDs(c.input) + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("parseNotificationIDs(%q) = %v, expected %v", c.input, actual, c.expected) + } + } +} + +func TestJoinNotificationIDs(t *testing.T) { + if got := joinNotificationIDs([]int64{1, 2}); got != "1,2" { + t.Errorf("joinNotificationIDs = %q, expected %q", got, "1,2") + } + if got := joinNotificationIDs(nil); got != "" { + t.Errorf("joinNotificationIDs(nil) = %q, expected empty", got) + } +} + +func TestKeywordInvertMapping(t *testing.T) { + if keywordInvert("yes") { + t.Error("KeywordExists yes should map to invertKeyword false") + } + if !keywordInvert("no") { + t.Error("KeywordExists no should map to invertKeyword true") + } + if keywordInvert("") { + t.Error("empty KeywordExists should default to invertKeyword false") + } + if got := invertToKeywordExists(true); got != "no" { + t.Errorf("invertToKeywordExists(true) = %q, expected %q", got, "no") + } + if got := invertToKeywordExists(false); got != "yes" { + t.Errorf("invertToKeywordExists(false) = %q, expected %q", got, "yes") + } +} + +func TestNormalizeConfigDefaults(t *testing.T) { + normalized := normalizeConfig(nil) + if normalized.Interval != DefaultInterval || normalized.MonitorType != "http" || normalized.KeywordExists != "yes" || normalized.Notifications != "" { + t.Errorf("normalizeConfig(nil) did not apply defaults: %+v", normalized) + } + + normalized = normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{}) + if normalized.Interval != DefaultInterval || normalized.MonitorType != "http" || normalized.KeywordExists != "yes" { + t.Errorf("normalizeConfig(empty) did not apply defaults: %+v", normalized) + } +} + +func TestNormalizeConfigAppliesValues(t *testing.T) { + normalized := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{ + Interval: 120, + MonitorType: "Keyword", + KeywordExists: "No", + KeywordValue: "foo", + Notifications: "5,1", + }) + + if normalized.Interval != 120 { + t.Errorf("expected interval 120, got %d", normalized.Interval) + } + if normalized.MonitorType != "keyword" { + t.Errorf("expected lowercased monitor type keyword, got %q", normalized.MonitorType) + } + if normalized.KeywordExists != "no" { + t.Errorf("expected lowercased keyword exists no, got %q", normalized.KeywordExists) + } + if normalized.KeywordValue != "foo" { + t.Errorf("expected keyword value foo, got %q", normalized.KeywordValue) + } + // Canonical (sorted) form so "5,1" and "1-5" compare equal + if normalized.Notifications != "1,5" { + t.Errorf("expected canonical notifications 1,5, got %q", normalized.Notifications) + } +} + +func TestNormalizeConfigCanonicalNotifications(t *testing.T) { + commaSeparated := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{Notifications: "5,1"}) + dashSeparated := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{Notifications: "1-5"}) + if !reflect.DeepEqual(commaSeparated, dashSeparated) { + t.Errorf("notification formats should normalize to the same config: %+v vs %+v", commaSeparated, dashSeparated) + } +} + +func TestBuildKumaMonitorHttp(t *testing.T) { + normalized := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{Interval: 90, Notifications: "2,1"}) + + mon := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + httpMonitor, ok := mon.(*monitor.HTTP) + if !ok { + t.Fatalf("expected a monitor.HTTP, got %T", mon) + } + if httpMonitor.Name != "test" || httpMonitor.URL != "https://example.com" { + t.Errorf("monitor mapped incorrectly: %+v", httpMonitor) + } + if httpMonitor.Interval != 90 { + t.Errorf("expected interval 90, got %d", httpMonitor.Interval) + } + if !httpMonitor.IsActive { + t.Error("expected created monitor to be active") + } + if !reflect.DeepEqual(httpMonitor.NotificationIDs, []int64{1, 2}) { + t.Errorf("expected notification ids [1 2], got %v", httpMonitor.NotificationIDs) + } + if httpMonitor.Method != "GET" { + t.Errorf("expected method GET, got %q", httpMonitor.Method) + } +} + +func TestBuildKumaMonitorKeyword(t *testing.T) { + normalized := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{ + MonitorType: "keyword", + KeywordValue: "healthy", + KeywordExists: "no", + }) + + mon := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + keywordMonitor, ok := mon.(*monitor.HTTPKeyword) + if !ok { + t.Fatalf("expected a monitor.HTTPKeyword, got %T", mon) + } + if keywordMonitor.Keyword != "healthy" { + t.Errorf("expected keyword healthy, got %q", keywordMonitor.Keyword) + } + if !keywordMonitor.InvertKeyword { + t.Error("expected KeywordExists no to map to invertKeyword true") + } + if keywordMonitor.URL != "https://example.com" || keywordMonitor.Name != "test" { + t.Errorf("monitor mapped incorrectly: %+v", keywordMonitor) + } +} + +// unmarshalBase builds a monitor.Base the same way the client does when it +// receives a monitor list, so the mapper can be tested offline +func unmarshalBase(t *testing.T, raw string) monitor.Base { + t.Helper() + base := monitor.Base{} + if err := json.Unmarshal([]byte(raw), &base); err != nil { + t.Fatalf("failed to unmarshal test monitor: %v", err) + } + return base +} + +func TestKumaMonitorToBaseMonitorHttp(t *testing.T) { + base := unmarshalBase(t, `{"id":7,"type":"http","name":"test","url":"https://example.com","interval":120,"active":true,"notificationIDList":{"3":true}}`) + + m := kumaMonitorToBaseMonitor(base) + + if m.ID != "7" || m.Name != "test" || m.URL != "https://example.com" { + t.Errorf("monitor mapped incorrectly: %+v", m) + } + providerConfig, _ := m.Config.(*endpointmonitorv1alpha1.UptimeKumaConfig) + if providerConfig.Interval != 120 { + t.Errorf("expected interval 120, got %d", providerConfig.Interval) + } + if providerConfig.MonitorType != "http" { + t.Errorf("expected monitor type http, got %q", providerConfig.MonitorType) + } + if providerConfig.Notifications != "3" { + t.Errorf("expected notifications 3, got %q", providerConfig.Notifications) + } +} + +func TestKumaMonitorToBaseMonitorKeyword(t *testing.T) { + base := unmarshalBase(t, `{"id":9,"type":"keyword","name":"kw","url":"https://example.com","interval":60,"keyword":"healthy","invertKeyword":true}`) + + m := kumaMonitorToBaseMonitor(base) + + if m.ID != "9" || m.URL != "https://example.com" { + t.Errorf("monitor mapped incorrectly: %+v", m) + } + providerConfig, _ := m.Config.(*endpointmonitorv1alpha1.UptimeKumaConfig) + if providerConfig.MonitorType != "keyword" { + t.Errorf("expected monitor type keyword, got %q", providerConfig.MonitorType) + } + if providerConfig.KeywordValue != "healthy" { + t.Errorf("expected keyword value healthy, got %q", providerConfig.KeywordValue) + } + if providerConfig.KeywordExists != "no" { + t.Errorf("expected keyword exists no, got %q", providerConfig.KeywordExists) + } +} + +func TestEqual(t *testing.T) { + service := UpTimeKumaMonitorService{} + + oldMonitor := models.Monitor{ + Name: "test", + URL: "https://example.com", + Config: &endpointmonitorv1alpha1.UptimeKumaConfig{ + Interval: 120, + MonitorType: "keyword", + KeywordValue: "healthy", + KeywordExists: "yes", + Notifications: "5,1", + }, + } + + if !service.Equal(oldMonitor, oldMonitor) { + t.Error("identical monitors should be equal") + } + + differentInterval := oldMonitor + differentInterval.Config = &endpointmonitorv1alpha1.UptimeKumaConfig{ + Interval: 300, + MonitorType: "keyword", + KeywordValue: "healthy", + KeywordExists: "yes", + Notifications: "5,1", + } + if service.Equal(oldMonitor, differentInterval) { + t.Error("different interval should not be equal") + } + + differentURL := oldMonitor + differentURL.URL = "https://other.example.com" + if service.Equal(oldMonitor, differentURL) { + t.Error("different URL should not be equal") + } + + // A monitor read back from Kuma ("1,5" canonical) equals a CRD config using "5,1" + readBack := models.Monitor{ + Name: "test", + URL: "https://example.com", + Config: &endpointmonitorv1alpha1.UptimeKumaConfig{ + Interval: 120, + MonitorType: "keyword", + KeywordValue: "healthy", + KeywordExists: "yes", + Notifications: "1,5", + }, + } + if !service.Equal(readBack, oldMonitor) { + t.Error("canonically equal notification lists should be equal") + } +} + +// Live integration tests below require a configured UptimeKuma provider in the +// test config; they skip silently when it is absent. + +func setupTestService(t *testing.T) *UpTimeKumaMonitorService { + configFilePath := os.Getenv("CONFIG_FILE_PATH") + if len(configFilePath) == 0 { + configFilePath = "../../../.local/test-config.yaml" + } + if _, err := os.Stat(configFilePath); err != nil { + // No test config available, skip the live test + return nil + } + + config := config.GetControllerConfigTest() + provider := util.GetProviderWithName(config, "UptimeKuma") + if provider == nil { + return nil + } + service := UpTimeKumaMonitorService{} + service.Setup(*provider) + return &service +} + +func TestUpTimeKumaMonitorLifecycle(t *testing.T) { + service := setupTestService(t) + if service == nil { + return + } + + m := models.Monitor{Name: "kuma-test", URL: "https://example.com"} + service.Add(m) + + mRes, err := service.GetByName("kuma-test") + if err != nil { + t.Error("Error: " + err.Error()) + } + if mRes == nil || mRes.Name != m.Name || mRes.URL != m.URL { + t.Errorf("monitor was not added correctly: %+v", mRes) + return + } + defer service.Remove(*mRes) + + mRes.URL = "https://other.example.com" + service.Update(*mRes) + + updated, err := service.GetByName("kuma-test") + if err != nil { + t.Error("Error: " + err.Error()) + } + if updated == nil || updated.URL != "https://other.example.com" { + t.Errorf("monitor was not updated correctly: %+v", updated) + } +} From 3832d8b641e72eb780d452c5dfb57d0f6b3bf16b Mon Sep 17 00:00:00 2001 From: davidgrldo Date: Thu, 1 Oct 2026 09:16:12 +0000 Subject: [PATCH 2/2] fix(uptimekuma): address review feedback on the Uptime Kuma provider Blockers: - pass context.Background() to kuma.New: the client keeps that context for the whole socket connection, so the deferred cancel closed it right after login and every call afterwards failed with "context canceled" - set retryInterval (Kuma rejects a monitor without one), maxredirects (10) and timeout (interval * 0.8) to the Kuma UI defaults, on create and on update; with maxredirects 0 every redirect was reported DOWN and with timeout 0 a hanging endpoint never went down - Dockerfile: golang:1.24 -> golang:1.25, the go.mod directive bump made `go mod download` fail - golangci-lint v1 is built with go 1.24 and refuses to lint a go 1.25 module, so bump the workflows and the Makefile to v2 and add a .golangci.yml that keeps the v1 default checks Behaviour: - do not create a keyword monitor without a keyword value, and return on a failed conversion of the current monitor in Update instead of continuing with an empty monitor - reconnect backoff: Uptime Kuma allows only 20 logins per minute for all clients, so a dropped connection is re-established after 5s, 10s, ... up to 5m, and an authentication error backs off for 5m with an actionable log - validate notification IDs against the notifications of the account: Kuma answers with a foreign key error but still creates the monitor without them, which made the controller retry a failing update on every reconcile Docs: - quote keywordExists/keywordValue in the example, the API server rejects the unquoted `no` (boolean) and `404` (number) - describe what keywordExists actually does and why it matches the UptimeRobot provider, and update the CRD description accordingly Tests: cover the retry interval, timeout and redirect defaults, the missing keyword value, the auth error detection, the reconnect backoff and the "not reconnecting yet" guard. --- .github/workflows/pull_request.yml | 5 +- .github/workflows/push.yml | 5 +- .golangci.yml | 29 +++ Dockerfile | 2 +- Makefile | 6 +- api/v1alpha1/endpointmonitor_types.go | 5 +- ...monitor.stakater.com_endpointmonitors.yaml | 7 +- ...monitor.stakater.com_endpointmonitors.yaml | 7 +- ...monitor.stakater.com_endpointmonitors.yaml | 7 +- docs/uptimekuma-configuration.md | 32 ++- .../controller/endpointmonitor_created.go | 2 +- .../appinsights/appinsights-monitor.go | 2 +- pkg/monitors/gcloud/gcloud-monitor.go | 2 +- pkg/monitors/grafana/grafana-monitor.go | 8 +- pkg/monitors/uptime/uptime-mappers_test.go | 4 +- pkg/monitors/uptimekuma/uptimekuma-monitor.go | 222 +++++++++++++++--- .../uptimekuma/uptimekuma-monitor_test.go | 112 ++++++++- .../uptimerobot/uptime-mappers_test.go | 2 +- .../uptimerobot/uptime-monitor_test.go | 6 +- 19 files changed, 400 insertions(+), 65 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0306a78c..4465f133 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -32,9 +32,10 @@ jobs: go-version-file: go.mod - name: Lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: - version: v1.64 + # v1 cannot lint a go 1.25 module, it is built with go 1.24 + version: v2.14.0 only-new-issues: false args: --timeout 10m diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index a30d39e1..eaca470e 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -41,9 +41,10 @@ jobs: go-version-file: go.mod - name: Lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: - version: v1.64 + # v1 cannot lint a go 1.25 module, it is built with go 1.24 + version: v2.14.0 only-new-issues: false args: --timeout 10m diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..95cdcab2 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,29 @@ +version: "2" + +# golangci-lint v2 is required to lint a go 1.25 module (v1 was built with go 1.24 +# and refuses to run on a newer go directive). The settings below keep the checks +# that were enabled by default in v1, so the migration does not turn on a new set +# of checks for existing code. +linters: + settings: + staticcheck: + checks: + - all + # quickfix checks (QF*) are suggestions, they were not part of the v1 + # default linter set + - -QF* + # checks stylecheck skipped by default in v1 + - -ST1000 + - -ST1003 + - -ST1016 + - -ST1020 + - -ST1021 + - -ST1022 + - -ST1023 + exclusions: + # the v1 `issues.exclude-use-default` defaults + presets: + - comments + - common-false-positives + - legacy + - std-error-handling diff --git a/Dockerfile b/Dockerfile index 2fc5cc68..e77d0bdd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24 AS builder +FROM golang:1.25 AS builder WORKDIR /workspace # Copy the Go Modules manifests diff --git a/Makefile b/Makefile index 18a52d0c..976dccc8 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ IMG ?= stakater/ingressmonitorcontroller:v2.2.4 # GOLANGCI_LINT env GOLANGCI_LINT = _output/tools/golangci-lint GOLANGCI_LINT_CACHE = $(PWD)/_output/golangci-lint-cache -GOLANGCI_LINT_VERSION = v1.64.0 +GOLANGCI_LINT_VERSION = v2.14.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -181,7 +181,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint KUSTOMIZE_VERSION ?= 5.4.3 CONTROLLER_TOOLS_VERSION ?= v0.16.1 ENVTEST_VERSION ?= release-0.19 -GOLANGCI_LINT_VERSION ?= v1.59.1 +GOLANGCI_LINT_VERSION ?= v2.14.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. @@ -201,7 +201,7 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary (ideally with version) diff --git a/api/v1alpha1/endpointmonitor_types.go b/api/v1alpha1/endpointmonitor_types.go index 80c5c425..9795d672 100644 --- a/api/v1alpha1/endpointmonitor_types.go +++ b/api/v1alpha1/endpointmonitor_types.go @@ -450,7 +450,10 @@ type UptimeKumaConfig struct { // +optional KeywordValue string `json:"keywordValue,omitempty"` - // Alert if value exist (yes) or doesn't exist (no) (Only if monitor-type is keyword) + // The monitor stays up while the keyword is present in the response (yes) or + // while it is absent (no), so `no` with a keyword like "404" alerts as soon + // as the endpoint starts answering with a 404 page (Only if monitor-type is + // keyword) // +kubebuilder:validation:Enum=yes;no // +optional KeywordExists string `json:"keywordExists,omitempty"` diff --git a/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml b/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml index 21bcf871..671c7106 100644 --- a/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/bundle/manifests/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -382,8 +382,11 @@ spec: minimum: 20 type: integer keywordExists: - description: Alert if value exist (yes) or doesn't exist (no) - (Only if monitor-type is keyword) + description: |- + The monitor stays up while the keyword is present in the response (yes) or + while it is absent (no), so `no` with a keyword like "404" alerts as soon + as the endpoint starts answering with a 404 page (Only if monitor-type is + keyword) enum: - "yes" - "no" diff --git a/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml b/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml index db792b68..eb44cb8e 100644 --- a/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -382,8 +382,11 @@ spec: minimum: 20 type: integer keywordExists: - description: Alert if value exist (yes) or doesn't exist (no) - (Only if monitor-type is keyword) + description: |- + The monitor stays up while the keyword is present in the response (yes) or + while it is absent (no), so `no` with a keyword like "404" alerts as soon + as the endpoint starts answering with a 404 page (Only if monitor-type is + keyword) enum: - "yes" - "no" diff --git a/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml b/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml index db792b68..eb44cb8e 100644 --- a/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml +++ b/config/crd/bases/endpointmonitor.stakater.com_endpointmonitors.yaml @@ -382,8 +382,11 @@ spec: minimum: 20 type: integer keywordExists: - description: Alert if value exist (yes) or doesn't exist (no) - (Only if monitor-type is keyword) + description: |- + The monitor stays up while the keyword is present in the response (yes) or + while it is absent (no), so `no` with a keyword like "404" alerts as soon + as the endpoint starts answering with a 404 page (Only if monitor-type is + keyword) enum: - "yes" - "no" diff --git a/docs/uptimekuma-configuration.md b/docs/uptimekuma-configuration.md index 1fc1cd3b..06939cc3 100644 --- a/docs/uptimekuma-configuration.md +++ b/docs/uptimekuma-configuration.md @@ -6,6 +6,7 @@ Notes: - Uptime Kuma API keys do **not** work for creating, updating or deleting monitors, so the controller authenticates with **username and password**. - **2FA must be disabled** for the account the controller uses — create a dedicated service account in Uptime Kuma without two-factor authentication. +- Uptime Kuma allows only 20 logins per minute for all clients together. The controller therefore keeps one connection for its lifetime and backs off for up to 5 minutes after a failed login, so a wrong password cannot lock out the Uptime Kuma UI. - Point `apiURL` at the internal HTTP service (e.g. `http://uptime-kuma.uptime-kuma.svc:3001`) rather than an ingress to avoid self-signed certificate issues. ## Configuration @@ -26,14 +27,39 @@ Additional uptime kuma configurations can be added to the EndpointMonitor throug |:--------------:|:--------------------------------------------------------------------------------------------------------:| | Interval | The uptime kuma check interval in seconds (minimum 20, defaults to 60) | | MonitorType | The uptime kuma monitor type (http or keyword) | -| KeywordExists | Alert if value exist (yes) or doesn't exist (no) (Only if monitor-type is keyword) | +| KeywordExists | `yes` keeps the monitor up while the keyword is present in the response, `no` keeps it up while the keyword is absent (Only if monitor-type is keyword) | | KeywordValue | keyword to check on URL (e.g.'search' or '404') (Only if monitor-type is keyword) | | Notifications | Comma-separated uptime kuma notification IDs to attach to this monitor | +### Keyword monitors + +A keyword monitor compares the response body against `keywordValue`: + +- `keywordExists: "yes"` (the default) — the monitor is **up** while the keyword is in the response and goes **down** when it disappears, for example `keywordValue: "search"` for a search page that is expected to render. +- `keywordExists: "no"` — the monitor is **up** only while the keyword is *absent*, so it goes **down** (and alerts) as soon as the keyword shows up, for example `keywordValue: "404"` to be alerted when the endpoint starts answering with a 404 page. + +This matches the `UptimeRobot` provider, where `keywordExists` selects between `keyword_type` 1 (contains) and 2 (does not contain). + +> **Note** +> `keywordExists` and `keywordValue` are strings. Quote them, otherwise YAML turns `no` into a boolean and `404` into a number and the API server rejects the resource. + +### Checking fields + +The controller sets the remaining Uptime Kuma check fields to the values the Kuma UI uses by default, since Uptime Kuma has no default for them and rejects a monitor without them: + +| Field | Value | +|:---------------:|:--------------------------| +| Retry interval | the same as the interval | +| Max redirects | 10 | +| Timeout | 80% of the interval | +| Accepted status | 200-299 | + ### Fetching notification IDs from Uptime Kuma Notification IDs are the identifiers of notification targets (e.g. a Slack or ntfy integration) configured in Uptime Kuma. You can find the ID of a notification in **Settings > Notifications** — it is shown in the URL when editing a notification, or in the notification list API. +The controller checks the configured IDs against the notifications of the account it logs in with and refuses to create or update a monitor if one of them does not exist. Uptime Kuma itself accepts such a monitor but silently drops the notifications, which made the controller retry a failing update on every reconcile. + ## Example ```yaml @@ -47,7 +73,7 @@ spec: uptimeKumaConfig: interval: 120 monitorType: keyword - keywordExists: no - keywordValue: 404 + keywordExists: "no" + keywordValue: "404" notifications: "1,2" ``` diff --git a/internal/controller/endpointmonitor_created.go b/internal/controller/endpointmonitor_created.go index bc00427e..b00c997b 100644 --- a/internal/controller/endpointmonitor_created.go +++ b/internal/controller/endpointmonitor_created.go @@ -10,7 +10,7 @@ import ( ) func (r *EndpointMonitorReconciler) handleCreate(request reconcile.Request, instance *endpointmonitorv1alpha1.EndpointMonitor, monitorName string, monitorService *monitors.MonitorServiceProxy) error { - log := r.Log.WithValues("Namespace", instance.ObjectMeta.Namespace) + log := r.Log.WithValues("Namespace", instance.Namespace) log.Info("Creating Monitor: "+monitorName, "MonitorType", monitorService.GetType()) diff --git a/pkg/monitors/appinsights/appinsights-monitor.go b/pkg/monitors/appinsights/appinsights-monitor.go index 56e35d1c..98b15cc3 100644 --- a/pkg/monitors/appinsights/appinsights-monitor.go +++ b/pkg/monitors/appinsights/appinsights-monitor.go @@ -201,7 +201,7 @@ func (aiService *AppinsightsMonitorService) GetByName(monitorName string) (*mode return nil, nil } } - return nil, fmt.Errorf("Error retrieving Application Insights WebTests %s (Resource Group %s): %v", monitorName, aiService.resourceGroup, err) + return nil, fmt.Errorf("error retrieving Application Insights WebTests %s (Resource Group %s): %v", monitorName, aiService.resourceGroup, err) } return &models.Monitor{ Name: *webtest.Name, diff --git a/pkg/monitors/gcloud/gcloud-monitor.go b/pkg/monitors/gcloud/gcloud-monitor.go index f1911305..83061540 100644 --- a/pkg/monitors/gcloud/gcloud-monitor.go +++ b/pkg/monitors/gcloud/gcloud-monitor.go @@ -57,7 +57,7 @@ func (service *MonitorService) GetByName(name string) (monitor *models.Monitor, break } if err != nil { - return nil, fmt.Errorf("Error Locating Monitor: %s", err.Error()) + return nil, fmt.Errorf("error locating monitor: %s", err.Error()) } if uptimeCheckConfig.DisplayName == name { localMonitor := transformToMonitor(uptimeCheckConfig) diff --git a/pkg/monitors/grafana/grafana-monitor.go b/pkg/monitors/grafana/grafana-monitor.go index 1de901b5..088e0e9d 100644 --- a/pkg/monitors/grafana/grafana-monitor.go +++ b/pkg/monitors/grafana/grafana-monitor.go @@ -37,7 +37,7 @@ func getID(monitor models.Monitor) (int64, error) { if len(monitor.ID) > 0 { idResult, err := strconv.ParseInt(monitor.ID, 10, 64) if err != nil { - return 0, fmt.Errorf("Error converting ID %v %v", monitor.ID, err) + return 0, fmt.Errorf("error converting ID %v %v", monitor.ID, err) } checkId = idResult } @@ -69,12 +69,12 @@ func (service *GrafanaMonitorService) CreateSyntheticCheck(monitor models.Monito availableProbes, err := service.smClient.ListProbes(service.ctx) if err != nil { - return nil, fmt.Errorf("Error listing probes %v", err) + return nil, fmt.Errorf("error listing probes %v", err) } var probeToSet []synthetic_monitoring.Probe var configProbeNames []string - var frequency int64 = service.frequency + frequency := service.frequency var alertSensitivity string providerConfig, _ := monitor.Config.(*endpointmonitorv1alpha1.GrafanaConfig) if providerConfig != nil { @@ -106,7 +106,7 @@ func (service *GrafanaMonitorService) CreateSyntheticCheck(monitor models.Monito checkId, err := getID(monitor) if err != nil { - return nil, fmt.Errorf("Error converting ID %v %v", monitor.ID, err) + return nil, fmt.Errorf("error converting ID %v %v", monitor.ID, err) } // Creating a new Check object return &synthetic_monitoring.Check{ diff --git a/pkg/monitors/uptime/uptime-mappers_test.go b/pkg/monitors/uptime/uptime-mappers_test.go index 9a87f3eb..483b8736 100644 --- a/pkg/monitors/uptime/uptime-mappers_test.go +++ b/pkg/monitors/uptime/uptime-mappers_test.go @@ -30,8 +30,8 @@ func TestUptimeMonitorMonitorToBaseMonitorMapper(t *testing.T) { if monitorObject.ID != strconv.Itoa(uptimeMonitorObject.PK) || monitorObject.Name != uptimeMonitorObject.Name || monitorObject.URL != uptimeMonitorObject.MspAddress || - 5 != providerConfig.Interval || - "HTTP" != providerConfig.CheckType { + providerConfig.Interval != 5 || + providerConfig.CheckType != "HTTP" { t.Error("Correct: \n", uptimeMonitorObject.Name, uptimeMonitorObject.PK, diff --git a/pkg/monitors/uptimekuma/uptimekuma-monitor.go b/pkg/monitors/uptimekuma/uptimekuma-monitor.go index e8f29d82..0053549c 100644 --- a/pkg/monitors/uptimekuma/uptimekuma-monitor.go +++ b/pkg/monitors/uptimekuma/uptimekuma-monitor.go @@ -2,6 +2,7 @@ package uptimekuma import ( "context" + "errors" "fmt" "reflect" "sort" @@ -26,17 +27,37 @@ const DefaultInterval = 60 const ( connectTimeout = 30 * time.Second requestTimeout = 30 * time.Second + + // Defaults matching the Uptime Kuma UI. Uptime Kuma rejects a monitor + // without a retry interval ("Retry interval cannot be less than 1 seconds") + // and treats a timeout of 0 as "wait interval x 800 seconds", which would + // keep a hanging endpoint UP forever. + defaultMaxRedirects = 10 + requestTimeoutFactor = 0.8 + + // Uptime Kuma only allows 20 logins per minute for all clients together, so + // a failed connection must not be retried on the next reconcile + initialReconnectBackoff = 5 * time.Second + maxReconnectBackoff = 5 * time.Minute + + // Bad credentials never fix themselves, so back off hard instead of + // repeatedly logging in and eventually locking out the Kuma UI as well + authErrorBackoff = 5 * time.Minute ) // UpTimeKumaMonitorService manages monitors on an Uptime Kuma server (Kuma 2.x) // via its socket.io API. One client connection is held for the lifetime of the -// service and lazily re-established after failures. +// service and lazily re-established after failures, honouring a backoff so a +// broken or misconfigured server does not exhaust Uptime Kuma's login limit. type UpTimeKumaMonitorService struct { url string username string password string - client *kuma.Client - mutex sync.Mutex + + client *kuma.Client + mutex sync.Mutex + reconnectAt time.Time + backoff time.Duration } func (service *UpTimeKumaMonitorService) Equal(oldMonitor models.Monitor, newMonitor models.Monitor) bool { @@ -48,10 +69,15 @@ func (service *UpTimeKumaMonitorService) Equal(oldMonitor models.Monitor, newMon } func (service *UpTimeKumaMonitorService) Setup(p config.Provider) { + service.mutex.Lock() + defer service.mutex.Unlock() + service.url = p.ApiURL service.username = p.Username service.password = p.Password service.client = nil + service.backoff = 0 + service.reconnectAt = time.Time{} } // ensureClient returns the connected client, (re)connecting on first use or @@ -60,34 +86,84 @@ func (service *UpTimeKumaMonitorService) ensureClient() (*kuma.Client, error) { service.mutex.Lock() defer service.mutex.Unlock() - if service.client == nil { - ctx, cancel := context.WithTimeout(context.Background(), connectTimeout) - defer cancel() + if service.client != nil { + return service.client, nil + } - client, err := kuma.New(ctx, service.url, service.username, service.password, - kuma.WithConnectTimeout(connectTimeout), - kuma.WithLogLevel(kuma.LogLevelNone), - ) - if err != nil { - return nil, fmt.Errorf("unable to connect to Uptime Kuma at %s: %w", service.url, err) - } - service.client = client + if wait := time.Until(service.reconnectAt); wait > 0 { + return nil, fmt.Errorf("not reconnecting to Uptime Kuma at %s yet, retrying in %s: the previous connection failed", + service.url, wait.Round(time.Second)) } + // The client keeps the context it was created with for the whole lifetime of + // the socket connection, so this must not be cancelled when this function + // returns. WithConnectTimeout bounds the connection attempt itself. + client, err := kuma.New(context.Background(), service.url, service.username, service.password, + kuma.WithConnectTimeout(connectTimeout), + kuma.WithLogLevel(kuma.LogLevelNone), + ) + if err != nil { + service.registerFailure(err) + return nil, fmt.Errorf("unable to connect to Uptime Kuma at %s: %w", service.url, err) + } + + service.client = client + service.backoff = 0 + service.reconnectAt = time.Time{} + return service.client, nil } -// invalidateClient drops the connection so the next call reconnects -func (service *UpTimeKumaMonitorService) invalidateClient() { +// invalidateClient drops the connection so the next call reconnects, and starts +// the reconnect backoff +func (service *UpTimeKumaMonitorService) invalidateClient(err error) { service.mutex.Lock() defer service.mutex.Unlock() if service.client != nil { - if err := service.client.Disconnect(); err != nil { - log.Error(err, "Failed to disconnect Uptime Kuma client") + if disconnectErr := service.client.Disconnect(); disconnectErr != nil { + log.Error(disconnectErr, "Failed to disconnect Uptime Kuma client") } service.client = nil } + + service.registerFailure(err) +} + +// registerFailure records a failed connection attempt and the earliest time a +// new one may be made. Callers must hold the mutex. +func (service *UpTimeKumaMonitorService) registerFailure(err error) { + if isAuthError(err) { + service.backoff = authErrorBackoff + log.Error(err, fmt.Sprintf("Authentication with Uptime Kuma at %s failed, not retrying for %s. Check the configured username and password, and that 2FA is disabled for that account", + service.url, service.backoff)) + } else { + if service.backoff == 0 { + service.backoff = initialReconnectBackoff + } else { + service.backoff *= 2 + } + if service.backoff > maxReconnectBackoff { + service.backoff = maxReconnectBackoff + } + log.Info(fmt.Sprintf("Connection to Uptime Kuma at %s lost, not reconnecting for %s", service.url, service.backoff)) + } + + service.reconnectAt = time.Now().Add(service.backoff) +} + +// isAuthError reports whether connecting failed because Uptime Kuma rejected the +// credentials, in which case reconnecting right away would only burn through the +// login rate limit. +func isAuthError(err error) bool { + if err == nil { + return false + } + + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "incorrect username or password") || + strings.Contains(msg, "autherrincorrectcreds") || + strings.Contains(msg, "login:") } func (service *UpTimeKumaMonitorService) GetAll() ([]models.Monitor, error) { @@ -101,7 +177,7 @@ func (service *UpTimeKumaMonitorService) GetAll() ([]models.Monitor, error) { kumaMonitors, err := client.GetMonitors(ctx) if err != nil { - service.invalidateClient() + service.invalidateClient(err) return nil, fmt.Errorf("unable to get monitors from Uptime Kuma: %w", err) } @@ -132,18 +208,29 @@ func (service *UpTimeKumaMonitorService) GetByName(name string) (*models.Monitor func (service *UpTimeKumaMonitorService) Add(m models.Monitor) { normalized := normalizeConfig(m.Config) + kumaMonitor, err := buildKumaMonitor(m, normalized) + if err != nil { + log.Error(err, "Monitor couldn't be added: "+m.Name) + return + } + client, err := service.ensureClient() if err != nil { log.Error(err, "Monitor couldn't be added: "+m.Name) return } + notificationIDs := parseNotificationIDs(normalized.Notifications) + if err := validateNotificationIDs(client, notificationIDs); err != nil { + log.Error(err, "Monitor couldn't be added: "+m.Name) + return + } + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) defer cancel() - _, err = client.CreateMonitor(ctx, buildKumaMonitor(m, normalized)) - if err != nil { - service.invalidateClient() + if _, err := client.CreateMonitor(ctx, kumaMonitor); err != nil { + service.invalidateClient(err) log.Error(err, "Monitor couldn't be added: "+m.Name) return } @@ -160,12 +247,23 @@ func (service *UpTimeKumaMonitorService) Update(m models.Monitor) { normalized := normalizeConfig(m.Config) + if normalized.MonitorType == "keyword" && len(normalized.KeywordValue) == 0 { + log.Error(nil, "Monitor couldn't be updated, it is of type keyword but the `keywordValue` is missing: "+m.Name) + return + } + client, err := service.ensureClient() if err != nil { log.Error(err, "Monitor couldn't be updated: "+m.Name) return } + notificationIDs := parseNotificationIDs(normalized.Notifications) + if err := validateNotificationIDs(client, notificationIDs); err != nil { + log.Error(err, "Monitor couldn't be updated: "+m.Name) + return + } + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) defer cancel() @@ -173,7 +271,7 @@ func (service *UpTimeKumaMonitorService) Update(m models.Monitor) { // monitor and override the fields managed by the controller current, err := client.GetMonitor(ctx, id) if err != nil { - service.invalidateClient() + service.invalidateClient(err) log.Error(err, "Monitor couldn't be updated: "+m.Name) return } @@ -183,11 +281,15 @@ func (service *UpTimeKumaMonitorService) Update(m models.Monitor) { keywordMonitor := monitor.HTTPKeyword{} if err := current.As(&keywordMonitor); err != nil { log.Error(err, "Unable to read current keyword monitor: "+m.Name) + return } keywordMonitor.Name = m.Name keywordMonitor.URL = m.URL keywordMonitor.Interval = int64(normalized.Interval) - keywordMonitor.NotificationIDs = parseNotificationIDs(normalized.Notifications) + keywordMonitor.RetryInterval = int64(normalized.Interval) + keywordMonitor.MaxRedirects = defaultMaxRedirects + keywordMonitor.Timeout = requestTimeoutFor(normalized.Interval) + keywordMonitor.NotificationIDs = notificationIDs keywordMonitor.Keyword = normalized.KeywordValue keywordMonitor.InvertKeyword = keywordInvert(normalized.KeywordExists) updated = &keywordMonitor @@ -195,16 +297,20 @@ func (service *UpTimeKumaMonitorService) Update(m models.Monitor) { httpMonitor := monitor.HTTP{} if err := current.As(&httpMonitor); err != nil { log.Error(err, "Unable to read current http monitor: "+m.Name) + return } httpMonitor.Name = m.Name httpMonitor.URL = m.URL httpMonitor.Interval = int64(normalized.Interval) - httpMonitor.NotificationIDs = parseNotificationIDs(normalized.Notifications) + httpMonitor.RetryInterval = int64(normalized.Interval) + httpMonitor.MaxRedirects = defaultMaxRedirects + httpMonitor.Timeout = requestTimeoutFor(normalized.Interval) + httpMonitor.NotificationIDs = notificationIDs updated = &httpMonitor } if err := client.UpdateMonitor(ctx, updated); err != nil { - service.invalidateClient() + service.invalidateClient(err) log.Error(err, "Monitor couldn't be updated: "+m.Name) return } @@ -229,7 +335,7 @@ func (service *UpTimeKumaMonitorService) Remove(m models.Monitor) { defer cancel() if err := client.DeleteMonitor(ctx, id); err != nil { - service.invalidateClient() + service.invalidateClient(err) log.Error(err, "Monitor couldn't be removed: "+m.Name) return } @@ -239,10 +345,11 @@ func (service *UpTimeKumaMonitorService) Remove(m models.Monitor) { // buildKumaMonitor builds a typed monitor for creation from the base monitor // and the normalized provider config -func buildKumaMonitor(m models.Monitor, normalized *endpointmonitorv1alpha1.UptimeKumaConfig) monitor.Monitor { +func buildKumaMonitor(m models.Monitor, normalized *endpointmonitorv1alpha1.UptimeKumaConfig) (monitor.Monitor, error) { base := monitor.Base{ Name: m.Name, Interval: int64(normalized.Interval), + RetryInterval: int64(normalized.Interval), NotificationIDs: parseNotificationIDs(normalized.Notifications), IsActive: true, } @@ -250,11 +357,15 @@ func buildKumaMonitor(m models.Monitor, normalized *endpointmonitorv1alpha1.Upti URL: m.URL, Method: "GET", AcceptedStatusCodes: []string{"200-299"}, + MaxRedirects: defaultMaxRedirects, + Timeout: requestTimeoutFor(normalized.Interval), } if normalized.MonitorType == "keyword" { if len(normalized.KeywordValue) == 0 { - log.Error(nil, "Monitor is of type Keyword but the `keyword-value` is missing") + // Creating the monitor with an empty keyword would leave a monitor + // that is UP or DOWN for the wrong reason + return nil, errors.New("monitor is of type keyword but the `keywordValue` is missing") } return &monitor.HTTPKeyword{ Base: base, @@ -263,13 +374,50 @@ func buildKumaMonitor(m models.Monitor, normalized *endpointmonitorv1alpha1.Upti Keyword: normalized.KeywordValue, InvertKeyword: keywordInvert(normalized.KeywordExists), }, - } + }, nil } return &monitor.HTTP{ Base: base, HTTPDetails: details, + }, nil +} + +// requestTimeoutFor returns the per-request timeout for a check interval, using +// the same 80% ratio as the Uptime Kuma UI. Without it Uptime Kuma waits +// interval x 800 seconds, so a hanging endpoint would never go down. +func requestTimeoutFor(interval int) int64 { + return int64(float64(interval) * requestTimeoutFactor) +} + +// validateNotificationIDs checks the configured notification IDs against the +// ones the connected account can see. Uptime Kuma rejects a monitor with an +// unknown notification ID with a foreign key error but still creates it without +// any notification, which made the controller retry a failing update on every +// reconcile. +func validateNotificationIDs(client *kuma.Client, ids []int64) error { + if len(ids) == 0 { + return nil + } + + known := make(map[int64]bool) + for _, notification := range client.GetNotifications(context.Background()) { + known[notification.GetID()] = true } + + missing := make([]string, 0, len(ids)) + for _, id := range ids { + if !known[id] { + missing = append(missing, strconv.FormatInt(id, 10)) + } + } + + if len(missing) > 0 { + return fmt.Errorf("unknown Uptime Kuma notification id(s): %s, find the available ids in Uptime Kuma under Settings > Notifications", + strings.Join(missing, ", ")) + } + + return nil } // kumaMonitorToBaseMonitor maps a monitor read from Uptime Kuma to the base @@ -357,8 +505,18 @@ func joinNotificationIDs(ids []int64) string { return strings.Join(strs, ",") } -// keywordInvert maps KeywordExists to Uptime Kuma's invertKeyword flag: -// "no" means alert when the keyword does NOT exist +// keywordInvert maps KeywordExists to Uptime Kuma's invertKeyword flag. +// +// Uptime Kuma reports a keyword monitor as UP when the keyword was found and +// invertKeyword is false (`keywordFound === !invertKeyword` in its check +// handler), so: +// +// keywordExists: "yes" (default) -> UP while the keyword is in the response +// keywordExists: "no" -> UP only while the keyword is absent, which +// alerts as soon as the keyword shows up +// (e.g. keywordValue: "404") +// +// This matches the UptimeRobot provider's keyword_type 1 and 2. func keywordInvert(keywordExists string) bool { return strings.EqualFold(keywordExists, "no") } diff --git a/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go b/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go index 5e23703e..e1be341f 100644 --- a/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go +++ b/pkg/monitors/uptimekuma/uptimekuma-monitor_test.go @@ -2,9 +2,11 @@ package uptimekuma import ( "encoding/json" + "errors" "os" "reflect" "testing" + "time" "github.com/breml/go-uptime-kuma-client/monitor" endpointmonitorv1alpha1 "github.com/stakater/IngressMonitorController/v2/api/v1alpha1" @@ -120,7 +122,10 @@ func TestNormalizeConfigCanonicalNotifications(t *testing.T) { func TestBuildKumaMonitorHttp(t *testing.T) { normalized := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{Interval: 90, Notifications: "2,1"}) - mon := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + mon, err := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } httpMonitor, ok := mon.(*monitor.HTTP) if !ok { t.Fatalf("expected a monitor.HTTP, got %T", mon) @@ -131,6 +136,17 @@ func TestBuildKumaMonitorHttp(t *testing.T) { if httpMonitor.Interval != 90 { t.Errorf("expected interval 90, got %d", httpMonitor.Interval) } + // Uptime Kuma rejects a monitor without a retry interval + if httpMonitor.RetryInterval != 90 { + t.Errorf("expected retry interval 90, got %d", httpMonitor.RetryInterval) + } + // Uptime Kuma's UI defaults: follow 10 redirects and time out at 80% of the interval + if httpMonitor.MaxRedirects != 10 { + t.Errorf("expected max redirects 10, got %d", httpMonitor.MaxRedirects) + } + if httpMonitor.Timeout != 72 { + t.Errorf("expected timeout 72, got %d", httpMonitor.Timeout) + } if !httpMonitor.IsActive { t.Error("expected created monitor to be active") } @@ -149,7 +165,10 @@ func TestBuildKumaMonitorKeyword(t *testing.T) { KeywordExists: "no", }) - mon := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + mon, err := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } keywordMonitor, ok := mon.(*monitor.HTTPKeyword) if !ok { t.Fatalf("expected a monitor.HTTPKeyword, got %T", mon) @@ -163,6 +182,95 @@ func TestBuildKumaMonitorKeyword(t *testing.T) { if keywordMonitor.URL != "https://example.com" || keywordMonitor.Name != "test" { t.Errorf("monitor mapped incorrectly: %+v", keywordMonitor) } + if keywordMonitor.RetryInterval != DefaultInterval { + t.Errorf("expected retry interval %d, got %d", DefaultInterval, keywordMonitor.RetryInterval) + } + if keywordMonitor.MaxRedirects != 10 { + t.Errorf("expected max redirects 10, got %d", keywordMonitor.MaxRedirects) + } + if keywordMonitor.Timeout != 48 { + t.Errorf("expected timeout 48, got %d", keywordMonitor.Timeout) + } +} + +func TestBuildKumaMonitorKeywordWithoutValue(t *testing.T) { + normalized := normalizeConfig(&endpointmonitorv1alpha1.UptimeKumaConfig{MonitorType: "keyword"}) + + mon, err := buildKumaMonitor(models.Monitor{Name: "test", URL: "https://example.com"}, normalized) + if err == nil { + t.Fatal("expected an error for a keyword monitor without a keyword value") + } + if mon != nil { + t.Errorf("expected no monitor, got %+v", mon) + } +} + +func TestRequestTimeoutFor(t *testing.T) { + cases := map[int]int64{20: 16, 60: 48, 120: 96, 300: 240} + for interval, expected := range cases { + if got := requestTimeoutFor(interval); got != expected { + t.Errorf("requestTimeoutFor(%d) = %d, expected %d", interval, got, expected) + } + } +} + +func TestIsAuthError(t *testing.T) { + cases := []struct { + err error + expected bool + }{ + {nil, false}, + {errors.New("connect to server: i/o timeout"), false}, + {errors.New("login: Incorrect username or password"), true}, + {errors.New("login: authIncorrectCreds"), true}, + } + for _, c := range cases { + if got := isAuthError(c.err); got != c.expected { + t.Errorf("isAuthError(%v) = %v, expected %v", c.err, got, c.expected) + } + } +} + +func TestRegisterFailureBacksOff(t *testing.T) { + service := UpTimeKumaMonitorService{url: "http://kuma:3001"} + + // A dropped connection backs off exponentially, capped at maxReconnectBackoff + expected := initialReconnectBackoff + for i := 0; i < 10; i++ { + service.registerFailure(errors.New("connection reset")) + if service.backoff != expected { + t.Fatalf("attempt %d: backoff = %s, expected %s", i, service.backoff, expected) + } + if wait := time.Until(service.reconnectAt); wait <= 0 { + t.Fatalf("attempt %d: reconnectAt is not in the future", i) + } + expected *= 2 + if expected > maxReconnectBackoff { + expected = maxReconnectBackoff + } + } + + // Credentials do not fix themselves, so auth errors get the long backoff + service.registerFailure(errors.New("login: Incorrect username or password")) + if service.backoff != authErrorBackoff { + t.Errorf("auth error backoff = %s, expected %s", service.backoff, authErrorBackoff) + } + if wait := time.Until(service.reconnectAt); wait <= 0 { + t.Error("reconnectAt is not in the future after an auth error") + } +} + +func TestEnsureClientHonoursBackoff(t *testing.T) { + service := UpTimeKumaMonitorService{url: "http://kuma:3001"} + service.reconnectAt = time.Now().Add(time.Minute) + + client, err := service.ensureClient() + if err == nil { + t.Fatal("expected an error while the reconnect backoff is active") + } + if client != nil { + t.Error("expected no client while the reconnect backoff is active") + } } // unmarshalBase builds a monitor.Base the same way the client does when it diff --git a/pkg/monitors/uptimerobot/uptime-mappers_test.go b/pkg/monitors/uptimerobot/uptime-mappers_test.go index d9e37947..cdb28303 100644 --- a/pkg/monitors/uptimerobot/uptime-mappers_test.go +++ b/pkg/monitors/uptimerobot/uptime-mappers_test.go @@ -26,7 +26,7 @@ func TestUptimeMonitorMonitorToBaseMonitorMapper(t *testing.T) { providerConfig, _ := monitorObject.Config.(*endpointmonitorv1alpha1.UptimeRobotConfig) - if monitorObject.ID != strconv.Itoa(uptimeMonitorObject.ID) || monitorObject.Name != uptimeMonitorObject.FriendlyName || monitorObject.URL != uptimeMonitorObject.URL || 900 != providerConfig.Interval { + if monitorObject.ID != strconv.Itoa(uptimeMonitorObject.ID) || monitorObject.Name != uptimeMonitorObject.FriendlyName || monitorObject.URL != uptimeMonitorObject.URL || providerConfig.Interval != 900 { t.Error("Mapper did not map the values correctly") } } diff --git a/pkg/monitors/uptimerobot/uptime-monitor_test.go b/pkg/monitors/uptimerobot/uptime-monitor_test.go index cdcf13a7..46d4bb57 100644 --- a/pkg/monitors/uptimerobot/uptime-monitor_test.go +++ b/pkg/monitors/uptimerobot/uptime-monitor_test.go @@ -146,7 +146,7 @@ func TestAddMonitorWithInterval(t *testing.T) { } providerConfig, _ := mRes.Config.(*endpointmonitorv1alpha1.UptimeRobotConfig) - if 600 != providerConfig.Interval { + if providerConfig.Interval != 600 { t.Error("The interval is incorrect, expected: 600, but was: " + strconv.Itoa(providerConfig.Interval)) } service.Remove(*mRes) @@ -183,7 +183,7 @@ func TestUpdateMonitorInterval(t *testing.T) { } providerConfig, _ := mRes.Config.(*endpointmonitorv1alpha1.UptimeRobotConfig) - if 600 != providerConfig.Interval { + if providerConfig.Interval != 600 { t.Error("The interval is incorrect, expected: 600, but was: " + strconv.Itoa(providerConfig.Interval)) } @@ -204,7 +204,7 @@ func TestUpdateMonitorInterval(t *testing.T) { providerConfig, _ = mRes.Config.(*endpointmonitorv1alpha1.UptimeRobotConfig) - if 900 != providerConfig.Interval { + if providerConfig.Interval != 900 { t.Error("The interval is incorrect, expected: 600, but was: " + strconv.Itoa(providerConfig.Interval)) }