diff --git a/Dockerfile b/Dockerfile index 801fc25d..911b32aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder COPY . /app/ WORKDIR /app/ -RUN go mod download +RUN go mod download -x ARG TARGETOS TARGETARCH RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o ./bin/version-checker ./cmd/. diff --git a/cmd/app/app.go b/cmd/app/app.go index beb7f8bc..e18d85f0 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -3,20 +3,27 @@ package app import ( "context" "fmt" + "net/http" + logrusr "github.com/bombsimon/logrusr/v4" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "github.com/go-chi/transport" "github.com/hashicorp/go-cleanhttp" - "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" // Load all auth plugins + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + ctrmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/client" "github.com/jetstack/version-checker/pkg/controller" "github.com/jetstack/version-checker/pkg/metrics" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) const ( @@ -38,23 +45,61 @@ func NewCommand(ctx context.Context) *cobra.Command { return fmt.Errorf("failed to parse --log-level %q: %s", opts.LogLevel, err) } - log := newLogger(logLevel) + + log := newLogger(logLevel).WithField("component", "controller") + ctrl.SetLogger(logrusr.New(log.WithField("controller", "manager").Logger)) + + defaultTestAllInfoMsg := fmt.Sprintf(`only containers with the annotation "%s/${my-container}=true" will be parsed`, api.EnableAnnotationKey) + if opts.DefaultTestAll { + defaultTestAllInfoMsg = fmt.Sprintf(`all containers will be tested, unless they have the annotation "%s/${my-container}=false"`, api.EnableAnnotationKey) + } restConfig, err := opts.kubeConfigFlags.ToRESTConfig() if err != nil { return fmt.Errorf("failed to build kubernetes rest config: %s", err) } - kubeClient, err := kubernetes.NewForConfig(restConfig) + log.Infof("flag --test-all-containers=%t %s", opts.DefaultTestAll, defaultTestAllInfoMsg) + + mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ + LeaderElection: false, + Metrics: server.Options{ + BindAddress: opts.MetricsServingAddress, + SecureServing: false, + }, + GracefulShutdownTimeout: &opts.GracefulShutdownTimeout, + Cache: cache.Options{SyncPeriod: &opts.CacheSyncPeriod}, + PprofBindAddress: opts.PprofBindAddress, + }) if err != nil { - return fmt.Errorf("failed to build kubernetes client: %s", err) + return err } - metricsServer := metrics.NewServer(log) - if err := metricsServer.Run(opts.MetricsServingAddress); err != nil { - return fmt.Errorf("failed to start metrics server: %s", err) + // Liveness probe + if err := mgr.AddMetricsServerExtraHandler("/healthz", + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })); err != nil { + log.Fatal("Unable to set up health check:", err) } + // Readiness probe + if err := mgr.AddMetricsServerExtraHandler("/readyz", + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if mgr.GetCache().WaitForCacheSync(context.Background()) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ready")) + } else { + http.Error(w, "cache not synced", http.StatusServiceUnavailable) + } + }), + ); err != nil { + log.Fatal("Unable to set up ready check:", err) + } + + metricsServer := metrics.New(log, ctrmetrics.Registry, mgr.GetCache()) + opts.Client.Transport = transport.Chain( cleanhttp.DefaultTransport(), metricsServer.RoundTripper, @@ -65,23 +110,24 @@ func NewCommand(ctx context.Context) *cobra.Command { return fmt.Errorf("failed to setup image registry clients: %s", err) } - defer func() { - if err := metricsServer.Shutdown(); err != nil { - log.Error(err) - } - }() + c := controller.NewPodReconciler(opts.CacheTimeout, + metricsServer, + client, + mgr.GetClient(), + log, + opts.DefaultTestAll, + ) - defaultTestAllInfoMsg := fmt.Sprintf(`only containers with the annotation "%s/${my-container}=true" will be parsed`, api.EnableAnnotationKey) - if opts.DefaultTestAll { - defaultTestAllInfoMsg = fmt.Sprintf(`all containers will be tested, unless they have the annotation "%s/${my-container}=false"`, api.EnableAnnotationKey) + if err := c.SetupWithManager(mgr); err != nil { + return err } - log.Infof("flag --test-all-containers=%t %s", opts.DefaultTestAll, defaultTestAllInfoMsg) - - c := controller.New(opts.CacheTimeout, metricsServer, - client, kubeClient, log, opts.DefaultTestAll) - - return c.Run(ctx, opts.CacheTimeout/2) + // Start the manager and all controllers + log.Info("Starting controller manager") + if err := mgr.Start(ctx); err != nil { + return err + } + return nil }, } diff --git a/cmd/app/options.go b/cmd/app/options.go index 1ac24a0c..b4bb191c 100644 --- a/cmd/app/options.go +++ b/cmd/app/options.go @@ -22,30 +22,30 @@ const ( envPrefix = "VERSION_CHECKER" envACRUsername = "ACR_USERNAME" - envACRPassword = "ACR_PASSWORD" - envACRRefreshToken = "ACR_REFRESH_TOKEN" + envACRPassword = "ACR_PASSWORD" // #nosec G101 + envACRRefreshToken = "ACR_REFRESH_TOKEN" // #nosec G101 envDockerUsername = "DOCKER_USERNAME" - envDockerPassword = "DOCKER_PASSWORD" - envDockerToken = "DOCKER_TOKEN" + envDockerPassword = "DOCKER_PASSWORD" // #nosec G101 + envDockerToken = "DOCKER_TOKEN" // #nosec G101 envECRIamRoleArn = "ECR_IAM_ROLE_ARN" - envECRAccessKeyID = "ECR_ACCESS_KEY_ID" - envECRSecretAccessKey = "ECR_SECRET_ACCESS_KEY" - envECRSessionToken = "ECR_SESSION_TOKEN" + envECRAccessKeyID = "ECR_ACCESS_KEY_ID" // #nosec G101 + envECRSecretAccessKey = "ECR_SECRET_ACCESS_KEY" // #nosec G101 + envECRSessionToken = "ECR_SESSION_TOKEN" // #nosec G101 - envGCRAccessToken = "GCR_TOKEN" + envGCRAccessToken = "GCR_TOKEN" // #nosec G101 - envGHCRAccessToken = "GHCR_TOKEN" + envGHCRAccessToken = "GHCR_TOKEN" // #nosec G101 envGHCRHostname = "GHCR_HOSTNAME" - envQuayToken = "QUAY_TOKEN" + envQuayToken = "QUAY_TOKEN" // #nosec G101 envSelfhostedPrefix = "SELFHOSTED" envSelfhostedUsername = "USERNAME" envSelfhostedPassword = "PASSWORD" envSelfhostedHost = "HOST" - envSelfhostedBearer = "TOKEN" + envSelfhostedBearer = "TOKEN" // #nosec G101 envSelfhostedTokenPath = "TOKEN_PATH" envSelfhostedInsecure = "INSECURE" envSelfhostedCAPath = "CA_PATH" @@ -68,6 +68,10 @@ type Options struct { CacheTimeout time.Duration LogLevel string + PprofBindAddress string + GracefulShutdownTimeout time.Duration + CacheSyncPeriod time.Duration + kubeConfigFlags *genericclioptions.ConfigFlags selfhosted selfhosted.Options @@ -105,6 +109,10 @@ func (o *Options) addAppFlags(fs *pflag.FlagSet) { "metrics-serving-address", "m", "0.0.0.0:8080", "Address to serve metrics on at the /metrics path.") + fs.StringVarP(&o.PprofBindAddress, + "pprof-serving-address", "", "", + "Address to serve pprof on for profiling.") + fs.BoolVarP(&o.DefaultTestAll, "test-all-containers", "a", false, "If enabled, all containers will be tested, unless they have the "+ @@ -118,6 +126,14 @@ func (o *Options) addAppFlags(fs *pflag.FlagSet) { fs.StringVarP(&o.LogLevel, "log-level", "v", "info", "Log level (debug, info, warn, error, fatal, panic).") + + fs.DurationVarP(&o.GracefulShutdownTimeout, + "graceful-shutdown-timeout", "", 10*time.Second, + "Time that the manager should wait for all controller to shutdown.") + + fs.DurationVarP(&o.CacheSyncPeriod, + "cache-sync-period", "", 5*time.Hour, + "The time in which all resources should be updated.") } func (o *Options) addAuthFlags(fs *pflag.FlagSet) { diff --git a/cmd/app/options_test.go b/cmd/app/options_test.go index 86796b00..3c10a7c0 100644 --- a/cmd/app/options_test.go +++ b/cmd/app/options_test.go @@ -170,8 +170,9 @@ func TestComplete(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { + os.Clearenv() for _, env := range test.envs { - os.Setenv(env[0], env[1]) + t.Setenv(env[0], env[1]) } o := new(Options) o.complete() diff --git a/deploy/charts/version-checker/README.md b/deploy/charts/version-checker/README.md index ecd849c2..a7eec703 100644 --- a/deploy/charts/version-checker/README.md +++ b/deploy/charts/version-checker/README.md @@ -22,6 +22,9 @@ A Helm chart for version-checker | additionalAnnotations | object | `{}` | Additional Annotations to apply to Service and Deployment/Pod Objects | | additionalLabels | object | `{}` | Additional Labels to apply to Service and Deployment/Pod Objects | | affinity | object | `{}` | Set affinity | +| dashboards.enabled | bool | `false` | Deploy Grafana Dashboard(s) for version-checker | +| dashboards.grafana | string | `""` | Grafana instance to associate the Dashboard with when using GrafanaOperator | +| dashboards.labels | object | `{}` | Additional labels to add to the Grafana Dashboard | | docker.password | string | `nil` | Password to authenticate with docker registry | | docker.token | string | `nil` | Token to authenticate with docker registry. Cannot be used with `docker.username` / `docker.password`. | | docker.username | string | `nil` | Username to authenticate with docker registry | diff --git a/deploy/charts/version-checker/dashboards/general-overview.json b/deploy/charts/version-checker/dashboards/general-overview.json new file mode 100644 index 00000000..b387d0d6 --- /dev/null +++ b/deploy/charts/version-checker/dashboards/general-overview.json @@ -0,0 +1,477 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 3, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "GitHub", + "tooltip": "View Github", + "type": "link", + "url": "https://github.com/jetstack/version-checker" + } + ], + "liveNow": true, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(count(version_checker_is_latest_version{exported_namespace=~\"$namespace\"}==0) by (image))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Need Attention", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(count(version_checker_is_latest_version{exported_namespace=~\"$namespace\"}==1) by (image))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Uptodate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "displayName": "%", + "mappings": [], + "max": 100, + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 40 + }, + { + "color": "green", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 3, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "round(count(version_checker_is_latest_version{exported_namespace=~\"$namespace\"}==1) / count(version_checker_is_latest_version{exported_namespace=~\"$namespace\"}) * 100,1)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Percentage Uptodate", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": true + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "0": { + "color": "dark-red", + "index": 1, + "text": "No" + }, + "1": { + "color": "green", + "index": 0, + "text": "Yes" + } + }, + "type": "value" + } + ], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 22, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": [], + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "UptoDate" + } + ] + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sort(sum by(image,current_version,latest_version) (version_checker_is_latest_version{exported_namespace=~\"$namespace\"}))", + "format": "table", + "instant": true, + "legendFormat": "Up to date", + "range": false, + "refId": "A" + } + ], + "title": "Cluster Image Version Checks", + "transformations": [ + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "Value" + } + ] + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": { + "Time": 0, + "Value": 4, + "current_version": 2, + "image": 1, + "latest_version": 3 + }, + "renameByName": { + "Value": "UptoDate", + "current_version": "" + } + } + } + ], + "type": "table" + } + ], + "refresh": "auto", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "prometheus", + "value": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(version_checker_is_latest_version,exported_namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(version_checker_is_latest_version,exported_namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "15s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "Version Checker General Overview", + "uid": "eegwegrs8v7k0b", + "version": 7, + "weekStart": "" +} diff --git a/deploy/charts/version-checker/dashboards/internal.json b/deploy/charts/version-checker/dashboards/internal.json new file mode 100644 index 00000000..0a4d4e9e --- /dev/null +++ b/deploy/charts/version-checker/dashboards/internal.json @@ -0,0 +1,2264 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 5, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "GitHub", + "tooltip": "View Github", + "type": "link", + "url": "https://github.com/jetstack/version-checker" + } + ], + "liveNow": true, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 22, + "panels": [], + "title": "Controller Runtime Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 0, + "y": 1 + }, + "id": 23, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(\n histogram_quantile(0.95, rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"pod\", namespace=\"$namespace\"}[5m]))\n)", + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "95th Percentile Reconciliation Duration", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 9, + "y": 1 + }, + "id": 26, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(workqueue_depth{controller=\"pod\",namespace=\"$namespace\"}) by (controller)", + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Workqueue Depth (Pending Items)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 24, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(rate(controller_runtime_reconcile_time_seconds_sum{namespace=\"$namespace\"}[5m]) / \nrate(controller_runtime_reconcile_time_seconds_count{namespace=\"$namespace\"}[5m])) by (controller)\n", + "instant": false, + "legendFormat": "{{controller}} {{result}}", + "range": true, + "refId": "A" + } + ], + "title": "Average Reconciliation Duration", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 27, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(rate(workqueue_adds_total{controller=\"pod\",namespace=\"$namespace\"}[5m])) by (controller)\n", + "instant": false, + "legendFormat": "{{controller}}", + "range": true, + "refId": "A" + } + ], + "title": "Queue Processing Rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": 3600000, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(rate(controller_runtime_reconcile_errors_total{namespace=\"$namespace\"}[$__rate_interval])) by (controller)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile Error Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(rate(controller_runtime_reconcile_total{controller=\"pod\", namespace=\"$namespace\"}[5m])) by (result)", + "instant": false, + "legendFormat": "{{result}}", + "range": true, + "refId": "A" + } + ], + "title": "Total number of reconciliations per controller.", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "increase(controller_runtime_reconcile_errors_total{controller=\"pod\",namespace=\"$namespace\"}[5m])\n", + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile Errors ", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 17, + "panels": [], + "title": "HTTP Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "description": "HTTP Requests by Domain along with inflight HTTP Requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(rate(http_client_in_flight_requests{namespace=~\"$namespace\", domain=~\"$domains\", code=\"OK\"}[1m]))", + "instant": false, + "interval": "3m", + "legendFormat": "Inflight", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "avg(rate(http_client_requests_total{namespace=\"$namespace\", domain=~\"$domains\", code=\"OK\"}[1m])) by (domain)", + "hide": false, + "instant": false, + "legendFormat": "{{domain}}", + "range": true, + "refId": "B" + } + ], + "title": "HTTP Successful Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": false, + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Inflight" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "quay.io / Unauthorized" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(http_client_in_flight_requests{namespace=~\"$namespace\"})", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "Inflight", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(increase(http_client_requests_total{namespace=\"$namespace\", domain=~\"$domains\", code!=\"OK\"}[10m])) by (domain,code)", + "hide": false, + "instant": false, + "legendFormat": "{{domain}} / {{code}}", + "range": true, + "refId": "B" + } + ], + "title": "HTTP Failed Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum(\n rate(http_dns_duration_seconds{event=\"dns_done\",namespace=~\"$namespace\",domain=~\"$domains\"}[1m])\n) by (domain)", + "hide": false, + "instant": false, + "legendFormat": "DNS: {{domain}}", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "avg(\n rate(http_tls_duration_seconds{event=\"tls_done\", namespace=~\"$namespace\"}[1m])\n)by (domain)", + "hide": false, + "instant": false, + "legendFormat": "TLS: {{domain}}", + "range": true, + "refId": "D" + } + ], + "title": "DNS / TLS Resolution", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 48 + }, + "id": 9, + "panels": [], + "title": "Core Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "rate(process_cpu_seconds_total{namespace=\"version-checker\"}[1m])", + "legendFormat": "CPU Seconds: {{pod}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "max(rate(process_cpu_seconds_total{namespace=\"version-checker\"}[1h])) by (pod)", + "hide": false, + "instant": false, + "legendFormat": "Max Hour: {{pod}}", + "range": true, + "refId": "B" + } + ], + "title": "Process CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "process_resident_memory_bytes{namespace=\"version-checker\"}", + "hide": false, + "instant": false, + "legendFormat": "Resident Memory", + "range": true, + "refId": "A" + } + ], + "title": "Process Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 58 + }, + "id": 10, + "panels": [], + "title": "Go Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 59 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "rate(go_gc_duration_seconds_sum{namespace=\"$namespace\"}[5m])", + "legendFormat": "GC Duration", + "range": true, + "refId": "B" + }, + { + "datasource": { + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "", + "legendFormat": "__auto", + "range": true, + "refId": "C" + } + ], + "title": "Go GC Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 59 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "go_goroutines{namespace=\"$namespace\"}", + "legendFormat": "Go Routines", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "go_threads{namespace=\"$namespace\"}", + "hide": false, + "instant": false, + "legendFormat": "Threads", + "range": true, + "refId": "B" + } + ], + "title": "Go Routines", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 68 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "go_memstats_heap_alloc_bytes{namespace=\"$namespace\", container=\"version-checker\"}", + "legendFormat": "Allocated Bytes", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "go_memstats_heap_idle_bytes{namespace=\"$namespace\", container=\"version-checker\"}", + "hide": false, + "instant": false, + "legendFormat": "Idle", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "go_memstats_heap_objects{namespace=\"$namespace\", container=\"version-checker\"}", + "hide": false, + "instant": false, + "legendFormat": "Objects", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "exemplar": false, + "expr": "go_memstats_heap_sys_bytes{namespace=\"$namespace\"}", + "hide": false, + "instant": false, + "legendFormat": "System", + "range": true, + "refId": "D" + } + ], + "title": "Go Heap Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 78 + }, + "id": 12, + "panels": [], + "title": "Container Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 79 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "time() - container_start_time_seconds{namespace=\"$namespace\",container!=\"POD\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Container Uptime", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 79 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "kube_deployment_status_replicas_available{namespace=~\"$namespace\"}", + "instant": false, + "legendFormat": "Available", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "kube_deployment_status_replicas_unavailable{namespace=~\"$namespace\"}", + "hide": false, + "instant": false, + "legendFormat": "Unavailable", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "kube_pod_container_status_restarts_total{namespace=~\"$namespace\"}", + "hide": false, + "instant": false, + "legendFormat": "Restarts", + "range": true, + "refId": "C" + } + ], + "title": "Replica Counts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 86 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum by (pod, container)(rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\",container=\"version-checker\"}[5m]))\n", + "instant": false, + "legendFormat": "{{pod}}/{{container}}", + "range": true, + "refId": "A" + } + ], + "title": "Container CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 86 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum by (pod, container)(container_memory_usage_bytes{container=\"version-checker\",namespace=~\"$namespace\"})", + "instant": false, + "legendFormat": "Usage: {{pod}}/{{container}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum by (pod, container)(container_memory_cache{container=\"version-checker\",namespace=~\"$namespace\"})", + "hide": true, + "instant": false, + "legendFormat": "Cache: {{pod}}/{{container}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum by (pod, container)(container_memory_rss{container=\"version-checker\",namespace=~\"$namespace\"})", + "hide": true, + "instant": false, + "legendFormat": "RSS: {{pod}}/{{container}}", + "range": true, + "refId": "C" + } + ], + "title": "Container Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": true, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Received.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 94 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "asc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum by (pod)(rate(container_network_transmit_bytes_total{namespace=~\"$namespace\",container=\"POD\"}[5m]))", + "instant": false, + "legendFormat": "Transmit: {{pod}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "editorMode": "code", + "expr": "sum by (pod)(rate(container_network_receive_bytes_total{namespace=~\"$namespace\",container=\"POD\"}[5m]))", + "hide": false, + "instant": false, + "legendFormat": "Received: {{pod}}", + "range": true, + "refId": "B" + } + ], + "title": "Container Network Traffic", + "type": "timeseries" + } + ], + "refresh": "auto", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "prometheus", + "value": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": [ + "version-checker" + ], + "value": [ + "version-checker" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(version_checker_is_latest_version,namespace)", + "hide": 0, + "includeAll": true, + "label": "Version Checker Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(version_checker_is_latest_version,namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "334e06ed-cbfd-4139-8151-9e7029478d14" + }, + "definition": "label_values(http_client_requests_total{namespace=\"$namespace\"},domain)", + "description": "Used for HTTP Metrics Drill down", + "hide": 0, + "includeAll": true, + "label": "Domains", + "multi": true, + "name": "domains", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(http_client_requests_total{namespace=\"$namespace\"},domain)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": { + "hidden": false, + "refresh_intervals": [ + "15s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "Version Checker - Internals", + "uid": "degwfag0en9xcd", + "version": 15, + "weekStart": "" +} diff --git a/deploy/charts/version-checker/templates/dashboards.yaml b/deploy/charts/version-checker/templates/dashboards.yaml new file mode 100644 index 00000000..7b01be80 --- /dev/null +++ b/deploy/charts/version-checker/templates/dashboards.yaml @@ -0,0 +1,56 @@ +{{ if .Values.dashboards.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "version-checker.name" . }}-dashboards + namespace: {{ .Release.Namespace }} + labels: + {{- include "version-checker.labels" . | nindent 4 }} + {{- if .Values.dashboards.labels }} + {{- .Values.dashboards.labels | toYaml }} + {{- end }} +data: + internal.json: |- + {{- .Files.Get "dashboards/internal.json" | nindent 4 }} + general-overview.json: |- + {{- .Files.Get "dashboards/general-overview.json" | nindent 4 }} +--- +{{ if (.Capabilities.APIVersions.Has "grafana.integreatly.org/v1beta1/GrafanaDashboard") }} +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: {{ include "version-checker.name" . }}-general + namespace: {{ .Release.Namespace }} + labels: + {{- include "version-checker.labels" . | nindent 4 }} + {{- if .Values.dashboards.labels }} + {{- .Values.dashboards.labels | toYaml }} + {{- end }} +spec: + instanceSelector: + matchLabels: + dashboards: "{{ .Values.dashboards.grafana | default "grafana" }}" + configMapRef: + name: {{ include "version-checker.name" . }}-dashboards + key: general-overview.json +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: {{ include "version-checker.name" . }}-internal + namespace: {{ .Release.Namespace }} + labels: + {{- include "version-checker.labels" . | nindent 4 }} + {{- if .Values.dashboards.labels }} + {{- .Values.dashboards.labels | toYaml }} + {{- end }} +spec: + instanceSelector: + matchLabels: + dashboards: "{{ .Values.dashboards.grafana | default "grafana" }}" + configMapRef: + name: {{ include "version-checker.name" . }}-dashboards + key: internal.json +{{- end -}} +{{- end -}} diff --git a/deploy/charts/version-checker/tests/dashboards_test.yaml b/deploy/charts/version-checker/tests/dashboards_test.yaml new file mode 100644 index 00000000..6bf1f038 --- /dev/null +++ b/deploy/charts/version-checker/tests/dashboards_test.yaml @@ -0,0 +1,64 @@ +suite: Grafana Dashboard +templates: + - dashboards.yaml +release: + name: version-checker + namespace: monitoring +set: + dashboards.enabled: true +tests: + - it: works + asserts: + - isKind: + of: ConfigMap + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: version-checker-dashboards + - isNotEmpty: + path: .data["general-overview.json"] + - isNotEmpty: + path: .data["internal.json"] + + - it: Works w/ GrafanaDashboard + capabilities: + apiVersions: + - grafana.integreatly.org/v1beta1/GrafanaDashboard + documentSelector: + matchMany: true + path: kind + value: GrafanaDashboard + asserts: + - hasDocuments: + count: 3 + - containsDocument: + any: true + name: version-checker-internal + namespace: monitoring + kind: GrafanaDashboard + apiVersion: grafana.integreatly.org/v1beta1 + - containsDocument: + any: true + name: version-checker-general + kind: GrafanaDashboard + apiVersion: grafana.integreatly.org/v1beta1 + - equal: + path: spec.instanceSelector.matchLabels.dashboards + value: grafana + - equal: + path: spec.configMapRef.name + value: version-checker-dashboards + + - documentSelector: + path: metadata.name + value: version-checker-internal + equal: + path: spec.configMapRef.key + value: internal.json + - documentSelector: + path: metadata.name + value: version-checker-general + equal: + path: spec.configMapRef.key + value: general-overview.json diff --git a/deploy/charts/version-checker/values.yaml b/deploy/charts/version-checker/values.yaml index 011b5e92..9ff718ee 100644 --- a/deploy/charts/version-checker/values.yaml +++ b/deploy/charts/version-checker/values.yaml @@ -212,6 +212,15 @@ prometheus: # -- ServiceAccount for new Prometheus Object serviceAccountName: prometheus +# Grafana Dashboards +dashboards: + # -- Deploy Grafana Dashboard(s) for version-checker + enabled: false + # -- Additional labels to add to the Grafana Dashboard + labels: {} + # -- Grafana instance to associate the Dashboard with when using GrafanaOperator + grafana: "" + # Configure a Prometheus-Operator ServiceMonitor object serviceMonitor: # -- Disable/Enable ServiceMonitor Object diff --git a/go.mod b/go.mod index 4a9802ce..14584f84 100644 --- a/go.mod +++ b/go.mod @@ -24,13 +24,14 @@ require ( k8s.io/cli-runtime v0.32.3 k8s.io/client-go v0.32.3 k8s.io/component-base v0.32.3 - k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect ) require ( - github.com/aws/aws-sdk-go-v2/config v1.29.9 - github.com/aws/aws-sdk-go-v2/credentials v1.17.62 + github.com/aws/aws-sdk-go-v2/config v1.29.12 + github.com/aws/aws-sdk-go-v2/credentials v1.17.65 github.com/aws/aws-sdk-go-v2/service/ecr v1.43.0 + github.com/bombsimon/logrusr/v4 v4.1.0 github.com/go-chi/transport v0.5.0 github.com/gofri/go-github-ratelimit v1.1.1 github.com/google/go-cmp v0.7.0 @@ -40,6 +41,7 @@ require ( github.com/jarcoal/httpmock v1.3.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/stretchr/testify v1.10.0 + sigs.k8s.io/controller-runtime v0.20.4 ) require ( @@ -54,8 +56,8 @@ require ( github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect github.com/aws/smithy-go v1.22.3 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -63,11 +65,13 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v28.0.2+incompatible // indirect + github.com/docker/cli v28.0.4+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect @@ -108,17 +112,19 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.6 // 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 gotest.tools/v3 v3.1.0 // indirect + k8s.io/apiextensions-apiserver v0.32.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/go.sum b/go.sum index 72c365ef..1d17bdfe 100644 --- a/go.sum +++ b/go.sum @@ -21,10 +21,10 @@ github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsf github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= -github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= +github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo= +github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= @@ -39,10 +39,10 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= @@ -51,6 +51,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+75kuh4= +github.com/bombsimon/logrusr/v4 v4.1.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= @@ -62,24 +64,32 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/cli v28.0.2+incompatible h1:cRPZ77FK3/IXTAIQQj1vmhlxiLS5m+MIUDwS6f57lrE= -github.com/docker/cli v28.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= +github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/transport v0.5.0 h1:xpnYcIOpBRrduJD68gX9YxkJouRGIE1y+rK5yGYnMXE= github.com/go-chi/transport v0.5.0/go.mod h1:uoCleTaQiFtoatEiiqcXFZ5OxIp6s1DfGeVsCVbalT4= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -173,10 +183,10 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 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= @@ -233,6 +243,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= @@ -256,8 +270,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -315,6 +329,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -331,6 +347,8 @@ gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= +k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= @@ -345,6 +363,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go new file mode 100644 index 00000000..df1231ee --- /dev/null +++ b/pkg/api/annotations.go @@ -0,0 +1,35 @@ +package api + +const ( + // EnableAnnotationKey is used for enabling or disabling version-checker for + // a given container. + EnableAnnotationKey = "enable.version-checker.io" + + // OverrideURLAnnotationKey is used to override the lookup URL. Useful when + // mirroring images. + OverrideURLAnnotationKey = "override-url.version-checker.io" + + // UseSHAAnnotationKey is used to comparing the SHA digests of images. This + // is silently set to true if the container image using using the SHA digest + // as its tag. + UseSHAAnnotationKey = "use-sha.version-checker.io" + + // MatchRegexAnnotationKey will enforce that tags that are looked up must + // match this regex. UseMetaDataAnnotationKey is not required when this is + // set. All other options are ignored when this is set. + MatchRegexAnnotationKey = "match-regex.version-checker.io" + + // UseMetaDataAnnotationKey is defined as a tag containing anything after the + // patch digit. + // e.g. v1.0.1-gke.3 v1.0.1-alpha.0, v1.2.3.4... + UseMetaDataAnnotationKey = "use-metadata.version-checker.io" + + // PinMajorAnnotationKey will pin the major version to check. + PinMajorAnnotationKey = "pin-major.version-checker.io" + + // PinMinorAnnotationKey will pin the minor version to check. + PinMinorAnnotationKey = "pin-minor.version-checker.io" + + // PinPatchAnnotationKey will pin the patch version to check. + PinPatchAnnotationKey = "pin-patch.version-checker.io" +) diff --git a/pkg/api/options.go b/pkg/api/options.go new file mode 100644 index 00000000..df7c8091 --- /dev/null +++ b/pkg/api/options.go @@ -0,0 +1,24 @@ +package api + +import "regexp" + +// Options is used to describe what restrictions should be used for determining +// the latest image. +type Options struct { + OverrideURL *string `json:"override-url,omitempty"` + + // UseSHA cannot be used with any other options + UseSHA bool `json:"use-sha,omitempty"` + + MatchRegex *string `json:"match-regex,omitempty"` + + // UseMetaData defines whether tags with '-alpha', '-debian.0' etc. is + // permissible. + UseMetaData bool `json:"use-metadata,omitempty"` + + PinMajor *int64 `json:"pin-major,omitempty"` + PinMinor *int64 `json:"pin-minor,omitempty"` + PinPatch *int64 `json:"pin-patch,omitempty"` + + RegexMatcher *regexp.Regexp `json:"-"` +} diff --git a/pkg/api/types.go b/pkg/api/types.go index cb2ddfb5..903745a1 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -2,65 +2,9 @@ package api import ( "context" - "regexp" "time" ) -const ( - // EnableAnnotationKey is used for enabling or disabling version-checker for - // a given container. - EnableAnnotationKey = "enable.version-checker.io" - - // OverrideURLAnnotationKey is used to override the lookup URL. Useful when - // mirroring images. - OverrideURLAnnotationKey = "override-url.version-checker.io" - - // UseSHAAnnotationKey is used to comparing the SHA digests of images. This - // is silently set to true if the container image using using the SHA digest - // as its tag. - UseSHAAnnotationKey = "use-sha.version-checker.io" - - // MatchRegexAnnotationKey will enforce that tags that are looked up must - // match this regex. UseMetaDataAnnotationKey is not required when this is - // set. All other options are ignored when this is set. - MatchRegexAnnotationKey = "match-regex.version-checker.io" - - // UseMetaDataAnnotationKey is defined as a tag containing anything after the - // patch digit. - // e.g. v1.0.1-gke.3 v1.0.1-alpha.0, v1.2.3.4... - UseMetaDataAnnotationKey = "use-metadata.version-checker.io" - - // PinMajorAnnotationKey will pin the major version to check. - PinMajorAnnotationKey = "pin-major.version-checker.io" - - // PinMinorAnnotationKey will pin the minor version to check. - PinMinorAnnotationKey = "pin-minor.version-checker.io" - - // PinPatchAnnotationKey will pin the patch version to check. - PinPatchAnnotationKey = "pin-patch.version-checker.io" -) - -// Options is used to describe what restrictions should be used for determining -// the latest image. -type Options struct { - OverrideURL *string `json:"override-url,omitempty"` - - // UseSHA cannot be used with any other options - UseSHA bool `json:"use-sha,omitempty"` - - MatchRegex *string `json:"match-regex,omitempty"` - - // UseMetaData defines whether tags with '-alpha', '-debian.0' etc. is - // permissible. - UseMetaData bool `json:"use-metadata,omitempty"` - - PinMajor *int64 `json:"pin-major,omitempty"` - PinMinor *int64 `json:"pin-minor,omitempty"` - PinPatch *int64 `json:"pin-patch,omitempty"` - - RegexMatcher *regexp.Regexp `json:"-"` -} - // ImageTag describes a container image tag. type ImageTag struct { Tag string `json:"tag"` diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index cfcca64b..22d39d2d 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -2,31 +2,20 @@ package cache import ( "context" - "sync" "time" "github.com/sirupsen/logrus" "github.com/jetstack/version-checker/pkg/api" + "github.com/patrickmn/go-cache" ) -// Cache is a generic cache store. +// Cache is a generic cache store - that supports a handler type Cache struct { - log *logrus.Entry - - mu sync.RWMutex - timeout time.Duration + log *logrus.Entry handler Handler - store map[string]*cacheItem -} - -// cacheItem is a single item for the cache stored. This cache item is -// periodically garbage collected. -type cacheItem struct { - mu sync.Mutex - timestamp time.Time - i interface{} + store *cache.Cache } // Handler is an interface for implementations of the cache fetch. @@ -37,73 +26,49 @@ type Handler interface { // New returns a new generic Cache. func New(log *logrus.Entry, timeout time.Duration, handler Handler) *Cache { - return &Cache{ + c := &Cache{ log: log.WithField("cache", "handler"), handler: handler, - timeout: timeout, - store: make(map[string]*cacheItem), + store: cache.New(timeout, timeout*2), } + // Set our Cleanup hook + c.store.OnEvicted(c.cleanup) + return c } -// Get returns the cache item from the store given the index. Will populate -// the cache if the index does not currently exist. -func (c *Cache) Get(ctx context.Context, index string, fetchIndex string, opts *api.Options) (interface{}, error) { - c.mu.RLock() - item, ok := c.store[index] - c.mu.RUnlock() +func (c *Cache) cleanup(key string, obj interface{}) { + c.log.Debugf("removing item from cache: %q", key) +} - // If the item doesn't yet exist, create a new zero item. - if !ok { - c.mu.Lock() - item = new(cacheItem) - c.store[index] = item - c.mu.Unlock() - } +func (c *Cache) Shutdown() { + c.store.Flush() +} - item.mu.Lock() - defer item.mu.Unlock() +// Get returns the cache item from the store given the index. Will populate +// the cache if the index does not currently exist. +func (c *Cache) Get(ctx context.Context, index string, fetchIndex string, opts *api.Options) (item interface{}, err error) { + item, found := c.store.Get(index) - // Test if exists in the cache or is too old - if item.timestamp.Add(c.timeout).Before(time.Now()) { + // If the item doesn't yet exist, Lets look it up + if !found { // Fetch a new item to commit - i, err := c.handler.Fetch(ctx, fetchIndex, opts) + item, err = c.handler.Fetch(ctx, fetchIndex, opts) if err != nil { return nil, err } // Commit to the cache c.log.Debugf("committing item: %q", index) - item.timestamp = time.Now() - item.i = i - - return i, nil + c.store.Set(index, item, cache.DefaultExpiration) } - c.log.Debugf("found: %q", index) - return item.i, nil + return item, err } -// StartGarbageCollector is a blocking func that will run the garbage collector -// against the cache. -func (c *Cache) StartGarbageCollector(refreshRate time.Duration) { - log := c.log.WithField("cache", "garbage_collector") - log.Infof("starting cache garbage collector") - ticker := time.NewTicker(refreshRate) - - for { - <-ticker.C - - c.mu.Lock() - - now := time.Now() - for index, item := range c.store { - if item.timestamp.Add(c.timeout).Before(now) { - log.Debugf("removing stale cache item: %q", index) - delete(c.store, index) - } - } - - c.mu.Unlock() - } +func (c *Cache) Update(index string, item interface{}) { + c.store.SetDefault(index, item) +} +func (c *Cache) Delete(index string) { + c.store.Delete(index) } diff --git a/pkg/client/acr/acr.go b/pkg/client/acr/acr.go index 3419ca7d..0f33800f 100644 --- a/pkg/client/acr/acr.go +++ b/pkg/client/acr/acr.go @@ -23,7 +23,6 @@ const ( ) type Client struct { - *http.Client Options cacheMu sync.Mutex @@ -54,10 +53,6 @@ type ManifestResponse struct { } func New(opts Options) (*Client, error) { - client := &http.Client{ - Timeout: time.Second * 5, - } - if len(opts.RefreshToken) > 0 && (len(opts.Username) > 0 || len(opts.Password) > 0) { return nil, errors.New("cannot specify refresh token as well as username/password") @@ -65,7 +60,6 @@ func New(opts Options) (*Client, error) { return &Client{ Options: opts, - Client: client, cachedACRClient: make(map[string]*acrClient), }, nil } @@ -212,7 +206,8 @@ func (c *Client) getAccessTokenClient(ctx context.Context, host string) (*acrCli } resp, err := autorest.SendWithSender(client, req, - autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...)) + autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...), + ) if err != nil { return nil, fmt.Errorf("%s: failed to request access token: %s", host, err) diff --git a/pkg/client/client.go b/pkg/client/client.go index 28a037eb..3b36edef 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -25,7 +25,8 @@ import ( type Client struct { clients []api.ImageClient fallbackClient api.ImageClient - log *logrus.Entry + + log *logrus.Entry } // Options used to configure client authentication. @@ -38,15 +39,25 @@ type Options struct { Quay quay.Options OCI oci.Options Selfhosted map[string]*selfhosted.Options - Transport http.RoundTripper + + Transport http.RoundTripper } func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) { + log = log.WithField("component", "client") + // Setup Transporters for all remaining clients (if one is set) + if opts.Transport != nil { + opts.Quay.Transporter = opts.Transport + opts.ECR.Transporter = opts.Transport + opts.GHCR.Transporter = opts.Transport + opts.GCR.Transporter = opts.Transport + } + acrClient, err := acr.New(opts.ACR) if err != nil { return nil, fmt.Errorf("failed to create acr client: %w", err) } - dockerClient, err := docker.New(ctx, opts.Docker) + dockerClient, err := docker.New(opts.Docker, log) if err != nil { return nil, fmt.Errorf("failed to create docker client: %w", err) } @@ -62,12 +73,30 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) selfhostedClients = append(selfhostedClients, sClient) } - fallbackClient, err := fallback.New(ctx, log, opts.Transport) + // Create some of the fallback clients + ociclient, err := oci.New(&opts.OCI) + if err != nil { + return nil, fmt.Errorf("failed to create OCI client: %w", err) + } + anonSelfHosted, err := selfhosted.New(ctx, log, &selfhosted.Options{Transporter: opts.Transport}) + if err != nil { + return nil, fmt.Errorf("failed to create anonymous Selfhosted client: %w", err) + } + annonDocker, err := docker.New(docker.Options{Transporter: opts.Transport}, log) + if err != nil { + return nil, fmt.Errorf("failed to create anonymous docker client: %w", err) + } + fallbackClient, err := fallback.New(ctx, log, []api.ImageClient{ + anonSelfHosted, + annonDocker, + ociclient, + }) if err != nil { return nil, fmt.Errorf("failed to create fallback client: %w", err) } c := &Client{ + // Append all the clients in order of which we want to check against clients: append( selfhostedClients, acrClient, @@ -78,7 +107,7 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) quay.New(opts.Quay, log), ), fallbackClient: fallbackClient, - log: log.WithField("client", "registry"), + log: log, } for _, client := range append(c.clients, fallbackClient) { diff --git a/pkg/client/docker/docker.go b/pkg/client/docker/docker.go index 06eeed13..12e34628 100644 --- a/pkg/client/docker/docker.go +++ b/pkg/client/docker/docker.go @@ -10,6 +10,9 @@ import ( "strings" "time" + "github.com/sirupsen/logrus" + + "github.com/hashicorp/go-retryablehttp" "github.com/jetstack/version-checker/pkg/api" ) @@ -30,32 +33,18 @@ type Client struct { Options } -type AuthResponse struct { - Token string `json:"token"` -} - -type TagResponse struct { - Next string `json:"next"` - Results []Result `json:"results"` -} - -type Result struct { - Name string `json:"name"` - Timestamp string `json:"last_updated"` - Images []Image `json:"images"` -} - -type Image struct { - Digest string `json:"digest"` - OS api.OS `json:"os"` - Architecture api.Architecture `json:"Architecture"` -} - -func New(ctx context.Context, opts Options) (*Client, error) { - client := &http.Client{ - Timeout: time.Second * 10, - Transport: opts.Transporter, +func New(opts Options, log *logrus.Entry) (*Client, error) { + ctx := context.Background() + retryclient := retryablehttp.NewClient() + if opts.Transporter != nil { + retryclient.HTTPClient.Transport = opts.Transporter } + retryclient.HTTPClient.Timeout = 10 * time.Second + retryclient.RetryMax = 10 + retryclient.RetryWaitMax = 2 * time.Minute + retryclient.RetryWaitMin = 1 * time.Second + retryclient.Logger = log.WithField("client", "docker") + client := retryclient.StandardClient() // Setup Auth if username and password used. if len(opts.Username) > 0 || len(opts.Password) > 0 { diff --git a/pkg/client/docker/types.go b/pkg/client/docker/types.go new file mode 100644 index 00000000..bfeaa994 --- /dev/null +++ b/pkg/client/docker/types.go @@ -0,0 +1,24 @@ +package docker + +import "github.com/jetstack/version-checker/pkg/api" + +type AuthResponse struct { + Token string `json:"token"` +} + +type TagResponse struct { + Next string `json:"next"` + Results []Result `json:"results"` +} + +type Result struct { + Name string `json:"name"` + Timestamp string `json:"last_updated"` + Images []Image `json:"images"` +} + +type Image struct { + Digest string `json:"digest"` + OS api.OS `json:"os"` + Architecture api.Architecture `json:"Architecture"` +} diff --git a/pkg/client/fallback/fallback.go b/pkg/client/fallback/fallback.go index 502025bf..12fc0c52 100644 --- a/pkg/client/fallback/fallback.go +++ b/pkg/client/fallback/fallback.go @@ -3,12 +3,9 @@ package fallback import ( "context" "fmt" - "net/http" "time" "github.com/jetstack/version-checker/pkg/api" - "github.com/jetstack/version-checker/pkg/client/oci" - "github.com/jetstack/version-checker/pkg/client/selfhosted" "github.com/patrickmn/go-cache" @@ -16,27 +13,17 @@ import ( ) type Client struct { - SelfHosted *selfhosted.Client - OCI *oci.Client - log *logrus.Entry - hostCache *cache.Cache -} + clients []api.ImageClient -func New(ctx context.Context, log *logrus.Entry, transporter http.RoundTripper) (*Client, error) { - sh, err := selfhosted.New(ctx, log, &selfhosted.Options{Transporter: transporter}) - if err != nil { - return nil, err - } - oci, err := oci.New(&oci.Options{Transporter: transporter}) - if err != nil { - return nil, err - } + log *logrus.Entry + hostCache *cache.Cache +} +func New(ctx context.Context, log *logrus.Entry, clients []api.ImageClient) (*Client, error) { return &Client{ - SelfHosted: sh, - OCI: oci, - hostCache: cache.New(5*time.Hour, 10*time.Hour), - log: log.WithField("client", "fallback"), + clients: clients, + hostCache: cache.New(5*time.Hour, 10*time.Hour), + log: log.WithField("client", "fallback"), }, nil } @@ -58,17 +45,19 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) (tags []api } c.log.Debugf("no client for host %s in cache, continuing fallback", host) - // Try selfhosted client first - if tags, err := c.SelfHosted.Tags(ctx, host, repo, image); err == nil { - c.hostCache.SetDefault(host, c.SelfHosted) - return tags, nil - } - c.log.Debug("failed to lookup via SelfHosted, looking up via OCI") + // Try clients, one by one until we have none left.. + for i, client := range c.clients { + if tags, err := client.Tags(ctx, host, repo, image); err == nil { + c.hostCache.SetDefault(host, client) + return tags, nil + } - // Fallback to OCI client - if tags, err := c.OCI.Tags(ctx, host, repo, image); err == nil { - c.hostCache.SetDefault(host, c.OCI) - return tags, nil + remaining := len(c.clients) - i - 1 + if remaining == 0 { + c.log.Debugf("failed to lookup via %q, Giving up, no more clients", client.Name()) + } else { + c.log.Debugf("failed to lookup via %q, continuing to search with %v clients remaining", client.Name(), remaining) + } } // If both clients fail, return an error @@ -79,6 +68,7 @@ func (c *Client) IsHost(_ string) bool { return true } +// Function only added to match ImageClient Interface func (c *Client) RepoImageFromPath(path string) (string, string) { - return c.SelfHosted.RepoImageFromPath(path) + return "", "" } diff --git a/pkg/client/gcr/gcr.go b/pkg/client/gcr/gcr.go index f28d5207..772c3ece 100644 --- a/pkg/client/gcr/gcr.go +++ b/pkg/client/gcr/gcr.go @@ -17,7 +17,8 @@ const ( ) type Options struct { - Token string + Token string + Transporter http.RoundTripper } type Client struct { @@ -38,7 +39,8 @@ func New(opts Options) *Client { return &Client{ Options: opts, Client: &http.Client{ - Timeout: time.Second * 5, + Timeout: time.Second * 5, + Transport: opts.Transporter, }, } } diff --git a/pkg/client/ghcr/ghcr.go b/pkg/client/ghcr/ghcr.go index a6028054..617e22d5 100644 --- a/pkg/client/ghcr/ghcr.go +++ b/pkg/client/ghcr/ghcr.go @@ -3,6 +3,7 @@ package ghcr import ( "context" "fmt" + "net/http" "net/url" "strings" @@ -13,8 +14,9 @@ import ( ) type Options struct { - Token string - Hostname string + Token string + Hostname string + Transporter http.RoundTripper } type Client struct { @@ -29,7 +31,7 @@ func New(opts Options) *Client { } ghRatelimitOpts := github_ratelimit.WithLimitDetectedCallback(rateLimitDetection) - ghRateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(nil, ghRatelimitOpts) + ghRateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(opts.Transporter, ghRatelimitOpts) if err != nil { panic(err) } @@ -107,7 +109,6 @@ func (c *Client) extractImageTags(versions []*github.PackageVersion) []api.Image var tags []api.ImageTag for _, ver := range versions { if meta, ok := ver.GetMetadata(); ok { - if len(meta.Container.Tags) == 0 { continue } diff --git a/pkg/client/oci/oci.go b/pkg/client/oci/oci.go index f27fa1b9..e67ed669 100644 --- a/pkg/client/oci/oci.go +++ b/pkg/client/oci/oci.go @@ -4,27 +4,26 @@ import ( "context" "fmt" "net/http" + "runtime" "strings" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/jetstack/version-checker/pkg/api" -) -type CredentialsMode int - -const ( - Auto CredentialsMode = iota - Multi - Single - Manual + "github.com/jetstack/version-checker/pkg/api" ) type Options struct { - // CredentailsMode CredentialsMode - // ServiceAccountName string - // ServiceAccountNamespace string Transporter http.RoundTripper + Auth *authn.AuthConfig +} + +func (o *Options) Authorization() (*authn.AuthConfig, error) { + if o.Auth != nil { + return o.Auth, nil + } + return authn.Anonymous.Authorization() } // Client is a client for a registry compatible with the OCI Distribution Spec @@ -35,7 +34,11 @@ type Client struct { // New returns a new client func New(opts *Options) (*Client, error) { - pullOpts := []remote.Option{} + pullOpts := []remote.Option{ + remote.WithJobs(runtime.NumCPU()), + remote.WithUserAgent("version-checker"), + remote.WithAuth(opts), + } if opts.Transporter != nil { pullOpts = append(pullOpts, remote.WithTransport(opts.Transporter)) } diff --git a/pkg/client/quay/quay.go b/pkg/client/quay/quay.go index 4e3c3145..7140418e 100644 --- a/pkg/client/quay/quay.go +++ b/pkg/client/quay/quay.go @@ -20,7 +20,8 @@ const ( ) type Options struct { - Token string + Token string + Transporter http.RoundTripper } type Client struct { @@ -62,6 +63,9 @@ func New(opts Options, log *logrus.Entry) *Client { client := retryablehttp.NewClient() client.RetryMax = 10 client.Logger = log.WithField("client", "quay") + if opts.Transporter != nil { + client.HTTPClient.Transport = opts.Transporter + } return &Client{ Options: opts, diff --git a/pkg/client/selfhosted/api_types.go b/pkg/client/selfhosted/api_types.go new file mode 100644 index 00000000..af205d2c --- /dev/null +++ b/pkg/client/selfhosted/api_types.go @@ -0,0 +1,29 @@ +package selfhosted + +import ( + "time" + + "github.com/jetstack/version-checker/pkg/api" +) + +type AuthResponse struct { + Token string `json:"token"` +} + +type TagResponse struct { + Tags []string `json:"tags"` +} + +type ManifestResponse struct { + Digest string + Architecture api.Architecture `json:"architecture"` + History []History `json:"history"` +} + +type History struct { + V1Compatibility string `json:"v1Compatibility"` +} + +type V1Compatibility struct { + Created time.Time `json:"created,omitempty"` +} diff --git a/pkg/client/selfhosted/selfhosted.go b/pkg/client/selfhosted/selfhosted.go index e2c25415..559eead7 100644 --- a/pkg/client/selfhosted/selfhosted.go +++ b/pkg/client/selfhosted/selfhosted.go @@ -58,28 +58,6 @@ type Client struct { httpScheme string } -type AuthResponse struct { - Token string `json:"token"` -} - -type TagResponse struct { - Tags []string `json:"tags"` -} - -type ManifestResponse struct { - Digest string - Architecture api.Architecture `json:"architecture"` - History []History `json:"history"` -} - -type History struct { - V1Compatibility string `json:"v1Compatibility"` -} - -type V1Compatibility struct { - Created time.Time `json:"created,omitempty"` -} - func New(ctx context.Context, log *logrus.Entry, opts *Options) (*Client, error) { client := &Client{ Client: &http.Client{ diff --git a/pkg/controller/checker/checker.go b/pkg/controller/checker/checker.go index aab44ad2..dcf42bbc 100644 --- a/pkg/controller/checker/checker.go +++ b/pkg/controller/checker/checker.go @@ -36,7 +36,6 @@ func (c *Checker) Container(ctx context.Context, log *logrus.Entry, container *corev1.Container, opts *api.Options, ) (*Result, error) { - statusSHA := containerStatusImageSHA(pod, container.Name) if len(statusSHA) == 0 { return nil, nil @@ -109,7 +108,6 @@ func (c *Checker) handleSemver(ctx context.Context, imageURL, statusSHA, current }, nil } - // isLatestOrEmptyTag will return true if the given tag is "" or "latest". func (c *Checker) isLatestOrEmptyTag(tag string) bool { return tag == "" || tag == "latest" diff --git a/pkg/controller/checker/helpers.go b/pkg/controller/checker/helpers.go index bdab9750..c5bf841d 100644 --- a/pkg/controller/checker/helpers.go +++ b/pkg/controller/checker/helpers.go @@ -1,30 +1,11 @@ package checker import ( - "encoding/json" - "fmt" - "hash/fnv" "strings" - "github.com/jetstack/version-checker/pkg/api" corev1 "k8s.io/api/core/v1" ) -// calculateHashIndex returns a hash index given an imageURL and options. -func calculateHashIndex(imageURL string, opts *api.Options) (string, error) { - optsJSON, err := json.Marshal(opts) - if err != nil { - return "", fmt.Errorf("failed to marshal options: %s", err) - } - - hash := fnv.New32() - if _, err := hash.Write(append(optsJSON, []byte(imageURL)...)); err != nil { - return "", fmt.Errorf("failed to calculate search hash: %s", err) - } - - return fmt.Sprintf("%d", hash.Sum32()), nil -} - // containerStatusImageSHA will return the containers image SHA, if it is ready. func containerStatusImageSHA(pod *corev1.Pod, containerName string) string { for _, status := range pod.Status.InitContainerStatuses { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go deleted file mode 100644 index 2705bed0..00000000 --- a/pkg/controller/controller.go +++ /dev/null @@ -1,189 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "time" - - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - corev1listers "k8s.io/client-go/listers/core/v1" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/workqueue" - "k8s.io/utils/clock" - - "github.com/jetstack/version-checker/pkg/client" - "github.com/jetstack/version-checker/pkg/controller/checker" - "github.com/jetstack/version-checker/pkg/controller/scheduler" - "github.com/jetstack/version-checker/pkg/controller/search" - "github.com/jetstack/version-checker/pkg/metrics" - "github.com/jetstack/version-checker/pkg/version" -) - -const ( - numWorkers = 10 -) - -// Controller is the main controller that check and exposes metrics on -// versions. -type Controller struct { - log *logrus.Entry - - kubeClient kubernetes.Interface - podLister corev1listers.PodLister - workqueue workqueue.TypedRateLimitingInterface[any] - scheduledWorkQueue scheduler.ScheduledWorkQueue - - metrics *metrics.Metrics - checker *checker.Checker - - defaultTestAll bool -} - -func New( - cacheTimeout time.Duration, - metrics *metrics.Metrics, - imageClient *client.Client, - kubeClient kubernetes.Interface, - log *logrus.Entry, - defaultTestAll bool, -) *Controller { - workqueue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()) - scheduledWorkQueue := scheduler.NewScheduledWorkQueue(clock.RealClock{}, workqueue.Add) - - log = log.WithField("module", "controller") - versionGetter := version.New(log, imageClient, cacheTimeout) - search := search.New(log, cacheTimeout, versionGetter) - - c := &Controller{ - log: log, - kubeClient: kubeClient, - workqueue: workqueue, - scheduledWorkQueue: scheduledWorkQueue, - metrics: metrics, - checker: checker.New(search), - defaultTestAll: defaultTestAll, - } - - return c -} - -// Run is a blocking func that will run the controller. -func (c *Controller) Run(ctx context.Context, cacheRefreshRate time.Duration) error { - defer c.workqueue.ShutDown() - - sharedInformerFactory := informers.NewSharedInformerFactoryWithOptions(c.kubeClient, time.Second*30) - c.podLister = sharedInformerFactory.Core().V1().Pods().Lister() - podInformer := sharedInformerFactory.Core().V1().Pods().Informer() - _, err := podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: c.addObject, - UpdateFunc: func(old, new interface{}) { - if key, err := cache.MetaNamespaceKeyFunc(old); err == nil { - c.scheduledWorkQueue.Forget(key) - } - c.addObject(new) - }, - DeleteFunc: c.deleteObject, - }) - if err != nil { - return fmt.Errorf("error creating podInformer: %s", err) - } - - c.log.Info("starting control loop") - sharedInformerFactory.Start(ctx.Done()) - if !cache.WaitForCacheSync(ctx.Done(), podInformer.HasSynced) { - return fmt.Errorf("error waiting for informer caches to sync") - } - - c.log.Info("starting workers") - // Launch 10 workers to process pod resources - for i := 0; i < numWorkers; i++ { - go wait.Until(func() { c.runWorker(ctx, cacheRefreshRate) }, time.Second, ctx.Done()) - } - - // Start image tag garbage collector - go c.checker.Search().Run(cacheRefreshRate) - - <-ctx.Done() - - return nil -} - -// runWorker is a long-running function that will continually call the -// processNextWorkItem function in order to read and process a message on the -// workqueue. -func (c *Controller) runWorker(ctx context.Context, searchReschedule time.Duration) { - for { - obj, shutdown := c.workqueue.Get() - if shutdown { - return - } - - key, ok := obj.(string) - if !ok { - return - } - - if err := c.processNextWorkItem(ctx, key, searchReschedule); err != nil { - c.log.Error(err.Error()) - } - } -} - -func (c *Controller) addObject(obj interface{}) { - key, err := cache.MetaNamespaceKeyFunc(obj) - if err != nil { - return - } - c.workqueue.AddRateLimited(key) -} - -func (c *Controller) deleteObject(obj interface{}) { - pod, ok := obj.(*corev1.Pod) - if !ok { - return - } - - for _, container := range pod.Spec.Containers { - c.log.WithFields( - logrus.Fields{"pod": pod.Name, "container": container.Name, "namespace": pod.Namespace}, - ).Debug("removing deleted pod containers from metrics") - c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, "init") - c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, "container") - } -} - -// processNextWorkItem will read a single work item off the workqueue and -// attempt to process it, by calling the syncHandler. -func (c *Controller) processNextWorkItem(ctx context.Context, key string, searchReschedule time.Duration) error { - defer c.workqueue.Done(key) - - namespace, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - c.log.Error(err, "invalid resource key") - return nil - } - - pod, err := c.podLister.Pods(namespace).Get(name) - if apierrors.IsNotFound(err) { - return nil - } - if err != nil { - return err - } - - if err := c.sync(ctx, pod); err != nil { - c.scheduledWorkQueue.Add(pod, time.Second*20) - return fmt.Errorf("error syncing '%s/%s': %s, requeuing", - pod.Name, pod.Namespace, err) - } - - // Check the image tag again after the cache timeout. - c.scheduledWorkQueue.Add(key, searchReschedule) - - return nil -} diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go deleted file mode 100644 index 7b9fa729..00000000 --- a/pkg/controller/controller_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package controller - -import ( - "context" - "io" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" - - "github.com/jetstack/version-checker/pkg/client" - "github.com/jetstack/version-checker/pkg/metrics" -) - -var testLogger = logrus.NewEntry(logrus.New()) - -func init() { - testLogger.Logger.SetOutput(io.Discard) -} - -func TestNewController(t *testing.T) { - kubeClient := fake.NewSimpleClientset() - metrics := &metrics.Metrics{} - imageClient := &client.Client{} - - controller := New(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) - - assert.NotNil(t, controller) - assert.Equal(t, controller.defaultTestAll, true) - assert.NotNil(t, controller.workqueue) - assert.NotNil(t, controller.checker) - assert.NotNil(t, controller.scheduledWorkQueue) -} - -func TestRun(t *testing.T) { - kubeClient := fake.NewSimpleClientset() - metrics := &metrics.Metrics{} - imageClient := &client.Client{} - controller := New(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) - - ctx, cancel := context.WithCancel(context.Background()) - - // Run the controller in a separate goroutine so the test doesn't block indefinitely - go func() { - err := controller.Run(ctx, 30*time.Second) - assert.NoError(t, err) - }() - - // Give the controller some time to start up and do initial processing - time.Sleep(1 * time.Second) - - // Cancel the context to stop the controller - cancel() - - // Wait a moment to ensure the controller has shut down - time.Sleep(1 * time.Second) - - // Example assertion: Ensure the Run method has exited (this is implicit by the test not timing out) - assert.True(t, true, "Controller should shutdown cleanly on context cancellation") - // You can also add assertions here if you want to validate any specific state after shutdown - assert.NotNil(t, controller.scheduledWorkQueue, "ScheduledWorkQueue should be initialized") -} - -func TestAddObject(t *testing.T) { - kubeClient := fake.NewSimpleClientset() - metrics := &metrics.Metrics{} - imageClient := &client.Client{} - controller := New(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) - - obj := &corev1.Pod{} - controller.addObject(obj) - - // Wait for the item to be added to the workqueue - key, _ := cache.MetaNamespaceKeyFunc(obj) - - // Retry a few times with a short sleep to ensure the item has been added - for i := 0; i < 10; i++ { - if controller.workqueue.Len() > 0 { - break - } - time.Sleep(10 * time.Millisecond) - } - - // Check the workqueue length and the added item - assert.Equal(t, 1, controller.workqueue.Len(), "Expected workqueue to have 1 item after adding an object") - - item, _ := controller.workqueue.Get() - assert.Equal(t, key, item, "Expected the workqueue item to match the object's key") -} - -func TestDeleteObject(t *testing.T) { - kubeClient := fake.NewSimpleClientset() - metrics := &metrics.Metrics{} - imageClient := &client.Client{} - controller := New(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) - - pod := &corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "container1"}, - {Name: "container2"}, - }, - }, - } - controller.deleteObject(pod) - - // We can't directly assert on log messages or metric updates, - // but we can ensure that no errors are thrown and the function executes. - assert.NotNil(t, pod) -} - -func TestProcessNextWorkItem(t *testing.T) { - kubeClient := fake.NewSimpleClientset() - metrics := &metrics.Metrics{} - imageClient := &client.Client{} - controller := New(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create a fake pod - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-pod", - Namespace: "default", - }, - } - - // Use a fake informer factory and lister - informerFactory := informers.NewSharedInformerFactory(kubeClient, time.Minute) - podInformer := informerFactory.Core().V1().Pods().Informer() - controller.podLister = informerFactory.Core().V1().Pods().Lister() - - // Add the pod to the fake informer - err := podInformer.GetIndexer().Add(pod) - assert.NoError(t, err) - - // Add the pod key to the workqueue - controller.workqueue.AddRateLimited("default/test-pod") - - // Start the informer to process the added pod - informerFactory.Start(ctx.Done()) - cache.WaitForCacheSync(ctx.Done(), podInformer.HasSynced) - - // Test the processNextWorkItem method - err = controller.processNextWorkItem(ctx, "default/test-pod", 30*time.Second) - assert.NoError(t, err) -} diff --git a/pkg/controller/pod_controller.go b/pkg/controller/pod_controller.go new file mode 100644 index 00000000..873260ea --- /dev/null +++ b/pkg/controller/pod_controller.go @@ -0,0 +1,107 @@ +package controller + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/jetstack/version-checker/pkg/client" + "github.com/jetstack/version-checker/pkg/controller/checker" + "github.com/jetstack/version-checker/pkg/controller/search" + "github.com/jetstack/version-checker/pkg/metrics" + "github.com/jetstack/version-checker/pkg/version" + + "github.com/sirupsen/logrus" +) + +const ( + numWorkers = 10 +) + +type PodReconciler struct { + k8sclient.Client + Log *logrus.Entry + Metrics *metrics.Metrics + VersionChecker *checker.Checker + RequeueDuration time.Duration // Configurable reschedule duration + + defaultTestAll bool +} + +func NewPodReconciler( + cacheTimeout time.Duration, + metrics *metrics.Metrics, + imageClient *client.Client, + kubeClient k8sclient.Client, + log *logrus.Entry, + defaultTestAll bool, +) *PodReconciler { + log = log.WithField("controller", "pod") + versionGetter := version.New(log, imageClient, cacheTimeout) + search := search.New(log, cacheTimeout, versionGetter) + + c := &PodReconciler{ + Log: log, + Client: kubeClient, + Metrics: metrics, + VersionChecker: checker.New(search), + defaultTestAll: defaultTestAll, + } + + return c +} + +// Reconcile is triggered whenever a watched object changes. +func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithField("pod", req.NamespacedName) + + // Fetch the Pod instance + pod := &corev1.Pod{} + err := r.Get(ctx, req.NamespacedName, pod) + if apierrors.IsNotFound(err) { + // Pod deleted, remove from metrics + log.Info("Pod not found, removing from metrics") + r.Metrics.RemovePod(req.Namespace, req.Name) + return ctrl.Result{Requeue: false}, nil + } + if err != nil { + return ctrl.Result{}, err + } + + // Perform the version check (your sync logic) + if err := r.sync(ctx, pod); err != nil { + log.Error(err, "Failed to process pod") + // Requeue after some time in case of failure + return ctrl.Result{RequeueAfter: (r.RequeueDuration / 2)}, nil + } + + // Schedule next check + return ctrl.Result{RequeueAfter: r.RequeueDuration}, nil +} + +// SetupWithManager initializes the controller +func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { + LeaderElect := false + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Pod{}, builder.OnlyMetadata). + WithOptions(controller.Options{MaxConcurrentReconciles: numWorkers, NeedLeaderElection: &LeaderElect}). + WithEventFilter(predicate.Funcs{ + CreateFunc: func(_ event.TypedCreateEvent[k8sclient.Object]) bool { return true }, + UpdateFunc: func(_ event.TypedUpdateEvent[k8sclient.Object]) bool { return true }, + DeleteFunc: func(e event.TypedDeleteEvent[k8sclient.Object]) bool { + r.Log.Infof("Pod deleted: %s/%s", e.Object.GetNamespace(), e.Object.GetName()) + r.Metrics.RemovePod(e.Object.GetNamespace(), e.Object.GetName()) + return false // Do not trigger reconciliation for deletes + }, + }). + Complete(r) +} diff --git a/pkg/controller/pod_controller_test.go b/pkg/controller/pod_controller_test.go new file mode 100644 index 00000000..ab33fb3e --- /dev/null +++ b/pkg/controller/pod_controller_test.go @@ -0,0 +1,131 @@ +package controller + +import ( + "context" + "io" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + + "github.com/prometheus/client_golang/prometheus" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/jetstack/version-checker/pkg/client" + "github.com/jetstack/version-checker/pkg/metrics" +) + +var testLogger = logrus.NewEntry(logrus.New()) + +func init() { + testLogger.Logger.SetOutput(io.Discard) +} + +func TestNewController(t *testing.T) { + kubeClient := fake.NewFakeClient() + metrics := metrics.New( + logrus.NewEntry(logrus.StandardLogger()), + prometheus.NewRegistry(), + kubeClient, + ) + imageClient := &client.Client{} + + controller := NewPodReconciler(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) + + assert.NotNil(t, controller) + assert.Equal(t, controller.defaultTestAll, true) + assert.Equal(t, controller.Client, kubeClient) + assert.NotNil(t, controller.VersionChecker) +} +func TestReconcile(t *testing.T) { + imageClient := &client.Client{} + + tests := []struct { + name string + pod *corev1.Pod + expectedError bool + expectedRequeue time.Duration + }{ + { + name: "Pod exists and is processed successfully", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + }, + expectedError: false, + expectedRequeue: 5 * time.Minute, + }, + { + name: "Pod does not exist", + pod: nil, + expectedError: false, + expectedRequeue: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewFakeClient() + metrics := metrics.New( + logrus.NewEntry(logrus.StandardLogger()), + prometheus.NewRegistry(), + kubeClient, + ) + controller := NewPodReconciler(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) + controller.RequeueDuration = 5 * time.Minute + + ctx := context.Background() + + if tt.pod != nil { + err := kubeClient.Create(ctx, tt.pod) + assert.NoError(t, err) + } + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: "test-pod", + Namespace: "default", + }, + } + + result, err := controller.Reconcile(ctx, req) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.expectedRequeue, result.RequeueAfter) + }) + } +} +func TestSetupWithManager(t *testing.T) { + kubeClient := fake.NewClientBuilder().Build() + metrics := metrics.New( + logrus.NewEntry(logrus.StandardLogger()), + prometheus.NewRegistry(), + kubeClient, + ) + imageClient := &client.Client{} + controller := NewPodReconciler(5*time.Minute, metrics, imageClient, kubeClient, testLogger, true) + + mgr, err := manager.New(&rest.Config{}, manager.Options{LeaderElectionConfig: nil}) + require.NoError(t, err) + + err = controller.SetupWithManager(mgr) + assert.NoError(t, err, "SetupWithManager should not return an error") +} diff --git a/pkg/controller/sync.go b/pkg/controller/pod_sync.go similarity index 80% rename from pkg/controller/sync.go rename to pkg/controller/pod_sync.go index 8778f092..b2e496f3 100644 --- a/pkg/controller/sync.go +++ b/pkg/controller/pod_sync.go @@ -15,8 +15,8 @@ import ( ) // sync will enqueue a given pod to run against the version checker. -func (c *Controller) sync(ctx context.Context, pod *corev1.Pod) error { - log := c.log.WithField("name", pod.Name).WithField("namespace", pod.Namespace) +func (c *PodReconciler) sync(ctx context.Context, pod *corev1.Pod) error { + log := c.Log.WithFields(logrus.Fields{"name": pod.Name, "namespace": pod.Namespace}) builder := options.New(pod.Annotations) @@ -41,16 +41,16 @@ func (c *Controller) sync(ctx context.Context, pod *corev1.Pod) error { } // syncContainer will enqueue a given container to check the version. -func (c *Controller) syncContainer(ctx context.Context, log *logrus.Entry, +func (c *PodReconciler) syncContainer(ctx context.Context, + log *logrus.Entry, builder *options.Builder, pod *corev1.Pod, container *corev1.Container, containerType string, ) error { - // If not enabled, exit early if !builder.IsEnabled(c.defaultTestAll, container.Name) { - c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, containerType) + c.Metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, containerType) return nil } @@ -79,22 +79,21 @@ func (c *Controller) syncContainer(ctx context.Context, log *logrus.Entry, // checkContainer will check the given container and options, and update // metrics according to the result. -func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, +func (c *PodReconciler) checkContainer(ctx context.Context, log *logrus.Entry, pod *corev1.Pod, container *corev1.Container, containerType string, opts *api.Options, ) error { - startTime := time.Now() defer func() { - c.metrics.RegisterImageDuration(pod.Namespace, pod.Name, container.Name, container.Image, startTime) + c.Metrics.RegisterImageDuration(pod.Namespace, pod.Name, container.Name, container.Image, startTime) }() - result, err := c.checker.Container(ctx, log, pod, container, opts) + result, err := c.VersionChecker.Container(ctx, log, pod, container, opts) if err != nil { // Report the error using ErrorsReporting - c.metrics.ErrorsReporting(pod.Namespace, pod.Name, container.Name, container.Image) + c.Metrics.ReportError(pod.Namespace, pod.Name, container.Name, container.Image) return err } @@ -111,7 +110,7 @@ func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, result.ImageURL, result.CurrentVersion, result.LatestVersion) } - c.metrics.AddImage(pod.Namespace, pod.Name, + c.Metrics.AddImage(pod.Namespace, pod.Name, container.Name, containerType, result.ImageURL, result.IsLatest, result.CurrentVersion, result.LatestVersion, diff --git a/pkg/controller/sync_test.go b/pkg/controller/pod_sync_test.go similarity index 79% rename from pkg/controller/sync_test.go rename to pkg/controller/pod_sync_test.go index a7fa876d..ff214d8b 100644 --- a/pkg/controller/sync_test.go +++ b/pkg/controller/pod_sync_test.go @@ -9,6 +9,9 @@ import ( "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/prometheus/client_golang/prometheus" "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/client" @@ -22,16 +25,21 @@ import ( // Test for the sync method. func TestController_Sync(t *testing.T) { t.Parallel() + log := logrus.NewEntry(logrus.New()) - metrics := metrics.NewServer(log) + metrics := metrics.New( + logrus.NewEntry(logrus.StandardLogger()), + prometheus.NewRegistry(), + fake.NewFakeClient(), + ) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) - controller := &Controller{ - log: log, - checker: checker, - metrics: metrics, + controller := &PodReconciler{ + Log: log, + VersionChecker: checker, + Metrics: metrics, defaultTestAll: true, } @@ -58,15 +66,15 @@ func TestController_Sync(t *testing.T) { func TestController_SyncContainer(t *testing.T) { t.Parallel() log := logrus.NewEntry(logrus.New()) - metrics := metrics.NewServer(log) + metrics := metrics.New(log, prometheus.NewRegistry(), fake.NewFakeClient()) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) - controller := &Controller{ - log: log, - checker: checker, - metrics: metrics, + controller := &PodReconciler{ + Log: log, + VersionChecker: checker, + Metrics: metrics, defaultTestAll: true, } @@ -90,15 +98,15 @@ func TestController_SyncContainer(t *testing.T) { func TestController_CheckContainer(t *testing.T) { t.Parallel() log := logrus.NewEntry(logrus.New()) - metrics := metrics.NewServer(log) + metrics := metrics.New(log, prometheus.NewRegistry(), fake.NewFakeClient()) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) - controller := &Controller{ - log: log, - checker: checker, - metrics: metrics, + controller := &PodReconciler{ + Log: log, + VersionChecker: checker, + Metrics: metrics, defaultTestAll: true, } @@ -120,15 +128,16 @@ func TestController_SyncContainer_NoVersionFound(t *testing.T) { t.Parallel() log := logrus.NewEntry(logrus.New()) - metrics := metrics.NewServer(log) + metrics := metrics.New(log, prometheus.NewRegistry(), fake.NewFakeClient()) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) - controller := &Controller{ - log: log, - checker: checker, - metrics: metrics, + controller := &PodReconciler{ + Log: log, + VersionChecker: checker, + Metrics: metrics, + defaultTestAll: true, } diff --git a/pkg/controller/scheduler/scheduler.go b/pkg/controller/scheduler/scheduler.go deleted file mode 100644 index c070abf4..00000000 --- a/pkg/controller/scheduler/scheduler.go +++ /dev/null @@ -1,91 +0,0 @@ -package scheduler - -import ( - "sync" - "time" - - "k8s.io/utils/clock" -) - -// For mocking purposes. -// This little bit of wrapping needs to be done because go doesn't do -// covariance, but it does coerce *time.Timer into stoppable implicitly if we -// write it out like so. -var afterFunc = func(c clock.Clock, d time.Duration, f func()) stoppable { - t := c.NewTimer(d) - - go func() { - defer t.Stop() - if ti := <-t.C(); ti == (time.Time{}) { - return - } - f() - }() - - return t -} - -// stoppable is the subset of time.Timer which we use, split out for mocking purposes. -type stoppable interface { - Stop() bool -} - -// ProcessFunc is a function to process an item in the work queue. -type ProcessFunc func(interface{}) - -// ScheduledWorkQueue is an interface to describe a queue that will execute the -// given ProcessFunc with the object given to Add once the time.Duration is up, -// since the time of calling Add. -type ScheduledWorkQueue interface { - // Add will add an item to this queue, executing the ProcessFunc after the - // Duration has come (since the time Add was called). If an existing Timer - // for obj already exists, the previous timer will be cancelled. - Add(interface{}, time.Duration) - // Forget will cancel the timer for the given object, if the timer exists. - Forget(interface{}) -} - -type scheduledWorkQueue struct { - processFunc ProcessFunc - clock clock.Clock - work map[interface{}]stoppable - workLock sync.Mutex -} - -// NewScheduledWorkQueue will create a new workqueue with the given processFunc. -func NewScheduledWorkQueue(clock clock.Clock, processFunc ProcessFunc) ScheduledWorkQueue { - return &scheduledWorkQueue{ - processFunc: processFunc, - clock: clock, - work: make(map[interface{}]stoppable), - workLock: sync.Mutex{}, - } -} - -// Add will add an item to this queue, executing the ProcessFunc after the -// Duration has come (since the time Add was called). If an existing Timer for -// obj already exists, the previous timer will be cancelled. -func (s *scheduledWorkQueue) Add(obj interface{}, duration time.Duration) { - s.workLock.Lock() - defer s.workLock.Unlock() - s.forget(obj) - s.work[obj] = afterFunc(s.clock, duration, func() { - defer s.Forget(obj) - s.processFunc(obj) - }) -} - -// Forget will cancel the timer for the given object, if the timer exists. -func (s *scheduledWorkQueue) Forget(obj interface{}) { - s.workLock.Lock() - defer s.workLock.Unlock() - s.forget(obj) -} - -// forget cancels and removes an item. It *must* be called with the lock already held. -func (s *scheduledWorkQueue) forget(obj interface{}) { - if timer, ok := s.work[obj]; ok { - timer.Stop() - delete(s.work, obj) - } -} diff --git a/pkg/controller/scheduler/scheduler_test.go b/pkg/controller/scheduler/scheduler_test.go deleted file mode 100644 index 0e50d508..00000000 --- a/pkg/controller/scheduler/scheduler_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package scheduler - -import ( - "sync" - "testing" - "time" - - "k8s.io/utils/clock" -) - -func TestAdd(t *testing.T) { - after := newMockAfter() - afterFunc = after.AfterFunc - - var wg sync.WaitGroup - type testT struct { - obj string - duration time.Duration - } - tests := []testT{ - {"test500", time.Millisecond * 500}, - {"test1000", time.Second * 1}, - {"test3000", time.Second * 3}, - } - for _, test := range tests { - wg.Add(1) - t.Run(test.obj, func(test testT) func(*testing.T) { - waitSubtest := make(chan struct{}) - return func(t *testing.T) { - startTime := after.currentTime - queue := NewScheduledWorkQueue(clock.RealClock{}, func(obj interface{}) { - defer wg.Done() - durationEarly := test.duration - after.currentTime.Sub(startTime) - - if durationEarly > 0 { - t.Errorf("got queue item %.2f seconds too early", float64(durationEarly)/float64(time.Second)) - } - if obj != test.obj { - t.Errorf("expected obj '%+v' but got obj '%+v'", test.obj, obj) - } - waitSubtest <- struct{}{} - }) - queue.Add(test.obj, test.duration) - after.warp(test.duration + time.Millisecond) - <-waitSubtest - } - }(test)) - } - - wg.Wait() -} - -func TestForget(t *testing.T) { - after := newMockAfter() - afterFunc = after.AfterFunc - - var wg sync.WaitGroup - type testT struct { - obj string - duration time.Duration - } - tests := []testT{ - {"test500", time.Millisecond * 500}, - {"test1000", time.Second * 1}, - {"test3000", time.Second * 3}, - } - for _, test := range tests { - wg.Add(1) - t.Run(test.obj, func(test testT) func(*testing.T) { - return func(t *testing.T) { - defer wg.Done() - queue := NewScheduledWorkQueue(clock.RealClock{}, func(_ interface{}) { - t.Errorf("scheduled function should never be called") - }) - queue.Add(test.obj, test.duration) - queue.Forget(test.obj) - after.warp(test.duration * 2) - } - }(test)) - } - - wg.Wait() -} - -// TestConcurrentAdd checks that if we add the same item concurrently, it -// doesn't end up hitting a data-race / leaking a timer. -func TestConcurrentAdd(t *testing.T) { - after := newMockAfter() - afterFunc = after.AfterFunc - var wg sync.WaitGroup - queue := NewScheduledWorkQueue(clock.RealClock{}, func(obj interface{}) { - t.Fatalf("should not be called, but was called with %v", obj) - }) - - for i := 0; i < 1000; i++ { - wg.Add(1) - go func() { - queue.Add(1, 1*time.Second) - wg.Done() - }() - } - wg.Wait() - - queue.Forget(1) - after.warp(5 * time.Second) -} - -type timerQueueItem struct { - f func() - t time.Time - run bool - stopped bool -} - -func (tq *timerQueueItem) Stop() bool { - stopped := tq.stopped - tq.stopped = true - return stopped -} - -type mockAfter struct { - lock *sync.Mutex - currentTime time.Time - queue []*timerQueueItem -} - -func newMockAfter() *mockAfter { - return &mockAfter{ - queue: make([]*timerQueueItem, 0), - lock: &sync.Mutex{}, - } -} - -func (m *mockAfter) AfterFunc(_ clock.Clock, d time.Duration, f func()) stoppable { - m.lock.Lock() - defer m.lock.Unlock() - - item := &timerQueueItem{ - f: f, - t: m.currentTime.Add(d), - } - m.queue = append(m.queue, item) - return item -} - -func (m *mockAfter) warp(d time.Duration) { - m.lock.Lock() - defer m.lock.Unlock() - m.currentTime = m.currentTime.Add(d) - for _, item := range m.queue { - if item.run || item.stopped { - continue - } - - if item.t.Before(m.currentTime) { - item.run = true - go item.f() - } - } -} diff --git a/pkg/controller/search/search.go b/pkg/controller/search/search.go index f5f80a26..fe2a528d 100644 --- a/pkg/controller/search/search.go +++ b/pkg/controller/search/search.go @@ -16,7 +16,6 @@ import ( // Searcher is the interface for Search to facilitate testing. type Searcher interface { - Run(time.Duration) LatestImage(context.Context, string, *api.Options) (*api.ImageTag, error) } @@ -66,12 +65,6 @@ func (s *Search) LatestImage(ctx context.Context, imageURL string, opts *api.Opt return lastestImage.(*api.ImageTag), nil } -// Run will run the search and image cache garbage collectors. -func (s *Search) Run(refreshRate time.Duration) { - go s.versionGetter.Run(refreshRate) - s.searchCache.StartGarbageCollector(refreshRate) -} - // calculateHashIndex returns a hash index given an imageURL and options. func calculateHashIndex(imageURL string, opts *api.Options) (string, error) { optsJSON, err := json.Marshal(opts) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 74d79c88..09aa10ee 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -2,54 +2,45 @@ package metrics import ( "context" - "fmt" - "net" - "net/http" - "strings" "sync" "time" "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" + ctrmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" ) // Metrics is used to expose container image version checks as prometheus // metrics. type Metrics struct { - *http.Server log *logrus.Entry - registry *prometheus.Registry + registry ctrmetrics.RegistererGatherer containerImageVersion *prometheus.GaugeVec containerImageChecked *prometheus.GaugeVec containerImageDuration *prometheus.GaugeVec containerImageErrors *prometheus.CounterVec + cache k8sclient.Reader + // Contains all metrics for the roundtripper roundTripper *RoundTripper - // container cache stores a cache of a container's current image, version, - // and the latest - containerCache map[string]cacheItem - mu sync.Mutex -} - -type cacheItem struct { - image string - currentVersion string - latestVersion string + mu sync.Mutex } -func NewServer(log *logrus.Entry) *Metrics { - // Reset the prometheus registry - reg := prometheus.NewRegistry() - - reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - reg.MustRegister(collectors.NewGoCollector()) +// func New(log *logrus.Entry, reg ctrmetrics.RegistererGatherer, kubeClient k8sclient.Client) *Metrics { +func New(log *logrus.Entry, reg ctrmetrics.RegistererGatherer, cache k8sclient.Reader) *Metrics { + // Attempt to register, but ignore errors + _ = reg.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + _ = reg.Register(collectors.NewGoCollector()) containerImageVersion := promauto.With(reg).NewGaugeVec( prometheus.GaugeOpts{ @@ -91,53 +82,19 @@ func NewServer(log *logrus.Entry) *Metrics { ) return &Metrics{ - log: log.WithField("module", "metrics"), + log: log.WithField("module", "metrics"), + cache: cache, + registry: reg, containerImageVersion: containerImageVersion, containerImageDuration: containerImageDuration, containerImageChecked: containerImageChecked, containerImageErrors: containerImageErrors, - containerCache: make(map[string]cacheItem), roundTripper: NewRoundTripper(reg), } } -// Run will run the metrics server. -func (m *Metrics) Run(servingAddress string) error { - router := http.NewServeMux() - router.Handle("/metrics", promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{})) - router.Handle("/healthz", http.HandlerFunc(m.healthzAndReadyzHandler)) - router.Handle("/readyz", http.HandlerFunc(m.healthzAndReadyzHandler)) - - ln, err := net.Listen("tcp", servingAddress) - if err != nil { - return err - } - - m.Server = &http.Server{ - Addr: ln.Addr().String(), - ReadTimeout: 8 * time.Second, - WriteTimeout: 8 * time.Second, - MaxHeaderBytes: 1 << 15, // 1 MiB - Handler: router, - } - - go func() { - m.log.Infof("serving metrics on %s/metrics", ln.Addr()) - - if err := m.Serve(ln); err != nil { - m.log.Errorf("failed to serve prometheus metrics: %s", err) - return - } - }() - - return nil -} - func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL string, isLatest bool, currentVersion, latestVersion string) { - // Remove old image url/version if it exists - m.RemoveImage(namespace, pod, container, containerType) - m.mu.Lock() defer m.mu.Unlock() @@ -154,51 +111,76 @@ func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL st m.containerImageChecked.With( m.buildLastUpdatedLabels(namespace, pod, container, containerType, imageURL), ).Set(float64(time.Now().Unix())) - - index := m.latestImageIndex(namespace, pod, container, containerType) - m.containerCache[index] = cacheItem{ - image: imageURL, - currentVersion: currentVersion, - latestVersion: latestVersion, - } } func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) { m.mu.Lock() defer m.mu.Unlock() + total := 0 - index := m.latestImageIndex(namespace, pod, container, containerType) - _, ok := m.containerCache[index] - if !ok { - return - } + total += m.containerImageVersion.DeletePartialMatch( + m.buildPartialLabels(namespace, pod), + ) + total += m.containerImageDuration.DeletePartialMatch( + m.buildPartialLabels(namespace, pod), + ) - m.containerImageVersion.DeletePartialMatch( + total += m.containerImageChecked.DeletePartialMatch( m.buildPartialLabels(namespace, pod), ) - m.containerImageDuration.DeletePartialMatch( + total += m.containerImageErrors.DeletePartialMatch( m.buildPartialLabels(namespace, pod), ) - m.containerImageChecked.DeletePartialMatch( + m.log.Infof("Removed %d metrics for image %s/%s/%s", total, namespace, pod, container) +} + +func (m *Metrics) RemovePod(namespace, pod string) { + m.mu.Lock() + defer m.mu.Unlock() + + total := 0 + total += m.containerImageVersion.DeletePartialMatch( + m.buildPartialLabels(namespace, pod), + ) + total += m.containerImageDuration.DeletePartialMatch( + m.buildPartialLabels(namespace, pod), + ) + total += m.containerImageChecked.DeletePartialMatch( + m.buildPartialLabels(namespace, pod), + ) + total += m.containerImageErrors.DeletePartialMatch( m.buildPartialLabels(namespace, pod), ) - delete(m.containerCache, index) + + m.log.Infof("Removed %d metrics for pod %s/%s", total, namespace, pod) } func (m *Metrics) RegisterImageDuration(namespace, pod, container, image string, startTime time.Time) { m.mu.Lock() defer m.mu.Unlock() - m.containerImageDuration.WithLabelValues(namespace, pod, container, image). - Set(time.Since(startTime).Seconds()) -} + if !m.PodExists(context.Background(), namespace, pod) { + m.log.WithField("metric", "RegisterImageDuration").Warnf("pod %s/%s not found, not registering error", namespace, pod) + return + } -func (m *Metrics) latestImageIndex(namespace, pod, container, containerType string) string { - return strings.Join([]string{namespace, pod, container, containerType}, "") + m.containerImageDuration.WithLabelValues( + namespace, pod, container, image, + ).Set(time.Since(startTime).Seconds()) } -func (m *Metrics) ErrorsReporting(namespace, pod, container, imageURL string) { - m.containerImageErrors.WithLabelValues(namespace, pod, container, imageURL).Inc() +func (m *Metrics) ReportError(namespace, pod, container, imageURL string) { + m.mu.Lock() + defer m.mu.Unlock() + + if !m.PodExists(context.Background(), namespace, pod) { + m.log.WithField("metric", "ReportError").Warnf("pod %s/%s not found, not registering error", namespace, pod) + return + } + + m.containerImageErrors.WithLabelValues( + namespace, pod, container, imageURL, + ).Inc() } func (m *Metrics) buildFullLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion string) prometheus.Labels { @@ -230,31 +212,9 @@ func (m *Metrics) buildPartialLabels(namespace, pod string) prometheus.Labels { } } -func (m *Metrics) Shutdown() error { - // If metrics server is not started than exit early - if m.Server == nil { - return nil - } - - m.log.Info("shutting down prometheus metrics server...") - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - if err := m.Server.Shutdown(ctx); err != nil { - return fmt.Errorf("prometheus metrics server shutdown failed: %s", err) - } - - m.log.Info("prometheus metrics server gracefully stopped") - - return nil -} - -func (m *Metrics) healthzAndReadyzHandler(w http.ResponseWriter, _ *http.Request) { - // Its not great, but does help ensure that we're alive and ready over - // calling the /metrics endpoint which can be expensive on large payloads - _, err := w.Write([]byte("OK")) - if err != nil { - m.log.Errorf("Failed to send Healthz/Readyz response: %s", err) - } +// This _should_ leverage the Controllers Cache +func (m *Metrics) PodExists(ctx context.Context, ns, name string) bool { + pod := &corev1.Pod{} + err := m.cache.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, pod) + return err == nil && pod.GetDeletionTimestamp() == nil } diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index e7c98d56..06b5f318 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -1,20 +1,30 @@ package metrics import ( + "context" "fmt" "testing" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/testutil" "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" ) +var fakek8s = fake.NewFakeClient() + func TestCache(t *testing.T) { - m := NewServer(logrus.NewEntry(logrus.New())) + m := New(logrus.NewEntry(logrus.New()), prometheus.NewRegistry(), fakek8s) // Lets add some Images/Metrics... for i, typ := range []string{"init", "container"} { @@ -25,10 +35,11 @@ func TestCache(t *testing.T) { // Check and ensure that the metrics are available... for i, typ := range []string{"init", "container"} { version := fmt.Sprintf("0.1.%d", i) - mt, err := m.containerImageVersion.GetMetricWith(m.buildFullLabels("namespace", "pod", "container", typ, "url", version, version)) - require.NoError(t, err) + mt, _ := m.containerImageVersion.GetMetricWith( + m.buildFullLabels("namespace", "pod", "container", typ, "url", version, version), + ) count := testutil.ToFloat64(mt) - assert.Equal(t, count, float64(1)) + assert.Equal(t, count, float64(1), "Expected to get a metric for containerImageVersion") } // as well as the lastUpdated... @@ -46,23 +57,24 @@ func TestCache(t *testing.T) { // Ensure metrics and values return 0 for i, typ := range []string{"init", "container"} { version := fmt.Sprintf("0.1.%d", i) - mt, err := m.containerImageVersion.GetMetricWith(m.buildFullLabels("namespace", "pod", "container", typ, "url", version, version)) - require.NoError(t, err) + mt, _ := m.containerImageVersion.GetMetricWith( + m.buildFullLabels("namespace", "pod", "container", typ, "url", version, version), + ) count := testutil.ToFloat64(mt) - assert.Equal(t, count, float64(0)) + assert.Equal(t, count, float64(0), "Expected NOT to get a metric for containerImageVersion") } // And the Last Updated is removed too for _, typ := range []string{"init", "container"} { mt, err := m.containerImageChecked.GetMetricWith(m.buildLastUpdatedLabels("namespace", "pod", "container", typ, "url")) require.NoError(t, err) count := testutil.ToFloat64(mt) - assert.Equal(t, count, float64(0)) + assert.Equal(t, count, float64(0), "Expected to get a metric for containerImageChecked") } } // TestErrorsReporting verifies that the error metric increments correctly func TestErrorsReporting(t *testing.T) { - m := NewServer(logrus.NewEntry(logrus.New())) + m := New(logrus.NewEntry(logrus.New()), prometheus.NewRegistry(), fakek8s) // Reset the metrics before testing m.containerImageErrors.Reset() @@ -81,8 +93,18 @@ func TestErrorsReporting(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("Case %d", i+1), func(t *testing.T) { + err := fakek8s.DeleteAllOf(context.Background(), &corev1.Pod{}) + require.NoError(t, err) + + // We need to ensure that the pod Exists! + err = fakek8s.Create(context.Background(), &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: tc.pod, Namespace: tc.namespace}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: tc.container, Image: tc.image}}}, + }) + require.NoError(t, err) + // Report an error - m.ErrorsReporting(tc.namespace, tc.pod, tc.container, tc.image) + m.ReportError(tc.namespace, tc.pod, tc.container, tc.image) // Retrieve metric metric, err := m.containerImageErrors.GetMetricWith(prometheus.Labels{ @@ -99,3 +121,66 @@ func TestErrorsReporting(t *testing.T) { }) } } + +func Test_Metrics_SkipOnDeletedPod(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + // Step 1: Create fake client with Pod + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + Namespace: "default", + UID: types.UID("test-uid"), + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build() + + // Step 2: Create Metrics with fake registry + reg := prometheus.NewRegistry() + log := logrus.NewEntry(logrus.New()) + metrics := New(log, reg, client) + + // verify Pod exists! + require.True(t, + metrics.PodExists(context.Background(), "default", "mypod"), + "Pod should exist at this point!", + ) + + // Register some metrics.... + metrics.RegisterImageDuration("default", "mypod", "mycontainer", "nginx:latest", time.Now()) + + // Step 3: Simulate a Delete occuring, Whilst still Reconciling... + _ = client.Delete(context.Background(), pod) + metrics.RemovePod("default", "mypod") + + // Step 4: Validate that all metrics have been removed... + metricFamilies, err := reg.Gather() + assert.NoError(t, err) + for _, mf := range metricFamilies { + assert.NotContains(t, *mf.Name, "is_latest_version", "Should not have been found: %+v", mf) + assert.NotContains(t, *mf.Name, "image_lookup_duration", "Should not have been found: %+v", mf) + assert.NotContains(t, *mf.Name, "image_failures_total", "Should not have been found: %+v", mf) + } + + // Register Error _after_ sync has completed! + metrics.ReportError("default", "mypod", "mycontianer", "nginx:latest") + + // Step 5: Attempt to register metrics (should not register anything) + require.False(t, + metrics.PodExists(context.Background(), "default", "mypod"), + "Pod should NOT exist at this point!", + ) + + metrics.RegisterImageDuration("default", "mypod", "mycontainer", "nginx:latest", time.Now()) + metrics.ReportError("default", "mypod", "mycontianer", "nginx:latest") + + // Step 6: Gather metrics and assert none were registered + metricFamilies, err = reg.Gather() + assert.NoError(t, err) + for _, mf := range metricFamilies { + assert.NotContains(t, *mf.Name, "is_latest_version", "Should not have been found: %+v", mf) + assert.NotContains(t, *mf.Name, "image_lookup_duration", "Should not have been found: %+v", mf) + assert.NotContains(t, *mf.Name, "image_failures_total", "Should not have been found: %+v", mf) + } +} diff --git a/pkg/metrics/roundtripper_test.go b/pkg/metrics/roundtripper_test.go index 19c153ba..7827fd88 100644 --- a/pkg/metrics/roundtripper_test.go +++ b/pkg/metrics/roundtripper_test.go @@ -11,6 +11,7 @@ import ( "github.com/go-chi/transport" "github.com/sirupsen/logrus" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" @@ -163,7 +164,7 @@ func TestRoundTripper(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - metricsServer := NewServer(log) + metricsServer := New(log, prometheus.NewRegistry(), fakek8s) server := httptest.NewServer(tt.handler) defer server.Close() @@ -175,6 +176,7 @@ func TestRoundTripper(t *testing.T) { require.NoError(t, err) resp, err := client.Do(req) + if tt.expectedError { assert.Error(t, err) assert.Nil(t, resp) @@ -183,6 +185,7 @@ func TestRoundTripper(t *testing.T) { assert.NotNil(t, resp) assert.Equal(t, tt.expectedStatus, resp.StatusCode) } + resp.Body.Close() // Validate metrics assert.NoError(t, diff --git a/pkg/version/version.go b/pkg/version/version.go index db0f33db..e2a2a266 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -36,11 +36,6 @@ func New(log *logrus.Entry, client *client.Client, cacheTimeout time.Duration) * return v } -// Run is a blocking func that will start the image cache garbage collector. -func (v *Version) Run(refreshRate time.Duration) { - v.imageCache.StartGarbageCollector(refreshRate) -} - // LatestTagFromImage will return the latest tag given an imageURL, according // to the given options. func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts *api.Options) (*api.ImageTag, error) {