Skip to content

Commit d006aae

Browse files
author
David Collom
committed
Track Kubernetes Channels for latest versions
1 parent 3dcfc81 commit d006aae

File tree

10 files changed

+244
-27
lines changed

10 files changed

+244
-27
lines changed

cmd/app/app.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,27 @@ func NewCommand(ctx context.Context) *cobra.Command {
110110
return fmt.Errorf("failed to setup image registry clients: %s", err)
111111
}
112112

113-
c := controller.NewPodReconciler(opts.CacheTimeout,
113+
_ = client
114+
115+
podController := controller.NewPodReconciler(opts.CacheTimeout,
114116
metricsServer,
115117
client,
116118
mgr.GetClient(),
117119
log,
118120
opts.DefaultTestAll,
119121
)
122+
if err := podController.SetupWithManager(mgr); err != nil {
123+
return err
124+
}
120125

121-
if err := c.SetupWithManager(mgr); err != nil {
126+
kubeController := controller.NewKubeReconciler(
127+
log,
128+
mgr.GetConfig(),
129+
metricsServer,
130+
opts.KubeInterval,
131+
opts.KubeChannel,
132+
)
133+
if err := mgr.Add(kubeController); err != nil {
122134
return err
123135
}
124136

cmd/app/options.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,16 @@ type Options struct {
7272
GracefulShutdownTimeout time.Duration
7373
CacheSyncPeriod time.Duration
7474

75+
KubeChannel string
76+
KubeInterval time.Duration
77+
78+
// kubeConfigFlags holds the flags for the kubernetes client
7579
kubeConfigFlags *genericclioptions.ConfigFlags
76-
selfhosted selfhosted.Options
7780

81+
// Client holds the options for the image client(s)
7882
Client client.Options
83+
// selfhosted holds the options for the selfhosted registry
84+
selfhosted selfhosted.Options
7985
}
8086

8187
func (o *Options) addFlags(cmd *cobra.Command) {
@@ -133,7 +139,15 @@ func (o *Options) addAppFlags(fs *pflag.FlagSet) {
133139

134140
fs.DurationVarP(&o.CacheSyncPeriod,
135141
"cache-sync-period", "", 5*time.Hour,
136-
"The time in which all resources should be updated.")
142+
"The duration in which all resources should be updated.")
143+
144+
fs.DurationVarP(&o.KubeInterval,
145+
"kube-interval", "", o.CacheSyncPeriod,
146+
"The time in which kubernetes channels updates are checked.")
147+
148+
fs.StringVarP(&o.KubeChannel,
149+
"kube-channel", "", "stable",
150+
"The Kubernetes channel to check against for cluster updates.")
137151
}
138152

139153
func (o *Options) addAuthFlags(fs *pflag.FlagSet) {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
)
2929

3030
require (
31+
github.com/Masterminds/semver/v3 v3.3.1
3132
github.com/aws/aws-sdk-go-v2/config v1.29.12
3233
github.com/aws/aws-sdk-go-v2/credentials v1.17.65
3334
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos
1919
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
2020
github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0=
2121
github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc=
22+
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
23+
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
2224
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
2325
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
2426
github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=

pkg/client/docker/docker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/hashicorp/go-retryablehttp"
1616
"github.com/jetstack/version-checker/pkg/api"
17+
"github.com/jetstack/version-checker/pkg/client/util"
1718
)
1819

1920
const (
@@ -43,6 +44,7 @@ func New(opts Options, log *logrus.Entry) (*Client, error) {
4344
retryclient.RetryMax = 10
4445
retryclient.RetryWaitMax = 2 * time.Minute
4546
retryclient.RetryWaitMin = 1 * time.Second
47+
retryclient.Backoff = util.HTTPBackOff
4648
retryclient.Logger = log.WithField("client", "docker")
4749
client := retryclient.StandardClient()
4850

pkg/client/fallback/fallback.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) (tags []api
5454

5555
remaining := len(c.clients) - i - 1
5656
if remaining == 0 {
57-
c.log.Debugf("failed to lookup via %q, Giving up, no more clients", client.Name())
57+
c.log.Infof("failed to lookup via %q, Giving up, no more clients", client.Name())
5858
} else {
59-
c.log.Debugf("failed to lookup via %q, continuing to search with %v clients remaining", client.Name(), remaining)
59+
c.log.Infof("failed to lookup via %q, continuing to search with %v clients remaining", client.Name(), remaining)
6060
}
6161
}
6262

pkg/client/util/http_backoff.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package util
2+
3+
import (
4+
"net/http"
5+
"time"
6+
7+
"github.com/hashicorp/go-retryablehttp"
8+
)
9+
10+
// This is a custom Backoff that enforces the Max wait duration.
11+
// If the sleep is greater we refuse to sleep at all
12+
func HTTPBackOff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
13+
sleep := retryablehttp.DefaultBackoff(min, max, attemptNum, resp)
14+
if sleep <= max {
15+
return sleep
16+
}
17+
18+
return 0
19+
}

pkg/controller/kube_controller.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"strings"
8+
"time"
9+
10+
"github.com/sirupsen/logrus"
11+
12+
"k8s.io/client-go/kubernetes"
13+
"k8s.io/client-go/rest"
14+
15+
"github.com/hashicorp/go-retryablehttp"
16+
"github.com/jetstack/version-checker/pkg/metrics"
17+
18+
"github.com/Masterminds/semver/v3"
19+
)
20+
21+
const channelURLSuffix = "https://dl.k8s.io/release/"
22+
23+
type ClusterVersionScheduler struct {
24+
client kubernetes.Interface
25+
log *logrus.Entry
26+
metrics *metrics.Metrics
27+
interval time.Duration
28+
channel string
29+
}
30+
31+
func NewKubeReconciler(
32+
log *logrus.Entry,
33+
config *rest.Config,
34+
metrics *metrics.Metrics,
35+
interval time.Duration,
36+
channel string,
37+
) *ClusterVersionScheduler {
38+
39+
return &ClusterVersionScheduler{
40+
log: log,
41+
client: kubernetes.NewForConfigOrDie(config),
42+
interval: interval,
43+
metrics: metrics,
44+
channel: channel,
45+
}
46+
}
47+
48+
func (s *ClusterVersionScheduler) Start(ctx context.Context) error {
49+
go s.runScheduler(ctx)
50+
return s.reconcile(ctx)
51+
}
52+
53+
func (s *ClusterVersionScheduler) runScheduler(ctx context.Context) {
54+
ticker := time.NewTicker(s.interval)
55+
defer ticker.Stop()
56+
57+
s.log.WithField("interval", s.interval).WithField("channel", s.channel).
58+
Info("ClusterVersionScheduler started")
59+
60+
for {
61+
select {
62+
case <-ctx.Done():
63+
s.log.Info("ClusterVersionScheduler stopping")
64+
return
65+
case <-ticker.C:
66+
if err := s.reconcile(ctx); err != nil {
67+
s.log.Error(err, "Failed to reconcile cluster version")
68+
}
69+
}
70+
}
71+
}
72+
73+
func (s *ClusterVersionScheduler) reconcile(_ context.Context) error {
74+
// Get current cluster version
75+
current, err := s.client.Discovery().ServerVersion()
76+
if err != nil {
77+
return fmt.Errorf("getting cluster version: %w", err)
78+
}
79+
80+
// Get latest stable version
81+
latest, err := getLatestStableVersion(s.channel)
82+
if err != nil {
83+
return fmt.Errorf("fetching latest stable version: %w", err)
84+
}
85+
86+
latestSemVer, err := semver.NewVersion(latest)
87+
if err != nil {
88+
return err
89+
}
90+
currentSemVer, err := semver.NewVersion(current.GitVersion)
91+
if err != nil {
92+
return err
93+
}
94+
// Strip metadata from the versions
95+
currentSemVerNoMeta, _ := currentSemVer.SetMetadata("")
96+
latestSemVerNoMeta, _ := latestSemVer.SetMetadata("")
97+
98+
// Register metrics!
99+
s.metrics.RegisterKubeVersion(!currentSemVerNoMeta.LessThan(&latestSemVerNoMeta),
100+
currentSemVerNoMeta.String(), latestSemVerNoMeta.String(),
101+
s.channel,
102+
)
103+
104+
s.log.WithFields(logrus.Fields{
105+
"currentVersion": currentSemVerNoMeta,
106+
"latestStable": latestSemVerNoMeta,
107+
"channel": s.channel,
108+
}).Info("Cluster version check complete")
109+
110+
return nil
111+
}
112+
113+
func getLatestStableVersion(channel string) (string, error) {
114+
if !strings.HasSuffix(channel, ".txt") {
115+
channel += ".txt"
116+
}
117+
118+
// We don't need a `/` here as its should be in the channelURLSuffix
119+
channelURL := fmt.Sprintf("%s%s", channelURLSuffix, channel)
120+
121+
client := retryablehttp.NewClient()
122+
client.RetryMax = 3
123+
client.RetryWaitMin = 1 * time.Second
124+
client.RetryWaitMax = 30 * time.Second
125+
// Optional: Log using your own logrus/logr logger
126+
client.Logger = nil
127+
128+
resp, err := client.Get(channelURL)
129+
if err != nil {
130+
return "", err
131+
}
132+
defer resp.Body.Close()
133+
134+
body, err := io.ReadAll(resp.Body)
135+
if err != nil {
136+
return "", err
137+
}
138+
139+
return strings.TrimSpace(string(body)), nil
140+
}

pkg/metrics/kubernetes.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package metrics
2+
3+
import "github.com/prometheus/client_golang/prometheus"
4+
5+
func (m *Metrics) RegisterKubeVersion(isLatest bool, currentVersion, latestVersion, channel string) {
6+
m.mu.Lock()
7+
defer m.mu.Unlock()
8+
9+
isLatestF := 0.0
10+
if isLatest {
11+
isLatestF = 1.0
12+
}
13+
14+
m.kubernetesVersion.With(
15+
prometheus.Labels{
16+
"current_version": currentVersion,
17+
"latest_version": latestVersion,
18+
"channel": channel,
19+
},
20+
).Set(isLatestF)
21+
}

pkg/metrics/metrics.go

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Metrics struct {
2828
containerImageDuration *prometheus.GaugeVec
2929
containerImageErrors *prometheus.CounterVec
3030

31+
// Kubernetes version metric
32+
kubernetesVersion *prometheus.GaugeVec
33+
3134
cache k8sclient.Reader
3235

3336
// Contains all metrics for the roundtripper
@@ -80,6 +83,16 @@ func New(log *logrus.Entry, reg ctrmetrics.RegistererGatherer, cache k8sclient.R
8083
"namespace", "pod", "container", "image",
8184
},
8285
)
86+
kubernetesVersion := promauto.With(reg).NewGaugeVec(
87+
prometheus.GaugeOpts{
88+
Namespace: "version_checker",
89+
Name: "is_latest_kube_version",
90+
Help: "Where the current cluster is using the latest release channel version",
91+
},
92+
[]string{
93+
"current_version", "latest_version", "channel",
94+
},
95+
)
8396

8497
return &Metrics{
8598
log: log.WithField("module", "metrics"),
@@ -90,6 +103,7 @@ func New(log *logrus.Entry, reg ctrmetrics.RegistererGatherer, cache k8sclient.R
90103
containerImageDuration: containerImageDuration,
91104
containerImageChecked: containerImageChecked,
92105
containerImageErrors: containerImageErrors,
106+
kubernetesVersion: kubernetesVersion,
93107
roundTripper: NewRoundTripper(reg),
94108
}
95109
}
@@ -113,15 +127,11 @@ func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL st
113127
).Set(float64(time.Now().Unix()))
114128
}
115129

116-
func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) {
117-
m.mu.Lock()
118-
defer m.mu.Unlock()
119-
total := 0
120-
121-
total += m.containerImageVersion.DeletePartialMatch(
130+
func (m *Metrics) CleanUpMetrics(namespace, pod string) (total int) {
131+
total += m.containerImageDuration.DeletePartialMatch(
122132
m.buildPartialLabels(namespace, pod),
123133
)
124-
total += m.containerImageDuration.DeletePartialMatch(
134+
total += m.containerImageChecked.DeletePartialMatch(
125135
m.buildPartialLabels(namespace, pod),
126136
)
127137

@@ -131,27 +141,23 @@ func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) {
131141
total += m.containerImageErrors.DeletePartialMatch(
132142
m.buildPartialLabels(namespace, pod),
133143
)
144+
return total
145+
}
146+
147+
func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) {
148+
m.mu.Lock()
149+
defer m.mu.Unlock()
150+
151+
total := m.CleanUpMetrics(namespace, pod)
152+
134153
m.log.Infof("Removed %d metrics for image %s/%s/%s", total, namespace, pod, container)
135154
}
136155

137156
func (m *Metrics) RemovePod(namespace, pod string) {
138157
m.mu.Lock()
139158
defer m.mu.Unlock()
140159

141-
total := 0
142-
total += m.containerImageVersion.DeletePartialMatch(
143-
m.buildPartialLabels(namespace, pod),
144-
)
145-
total += m.containerImageDuration.DeletePartialMatch(
146-
m.buildPartialLabels(namespace, pod),
147-
)
148-
total += m.containerImageChecked.DeletePartialMatch(
149-
m.buildPartialLabels(namespace, pod),
150-
)
151-
total += m.containerImageErrors.DeletePartialMatch(
152-
m.buildPartialLabels(namespace, pod),
153-
)
154-
160+
total := m.CleanUpMetrics(namespace, pod)
155161
m.log.Infof("Removed %d metrics for pod %s/%s", total, namespace, pod)
156162
}
157163

0 commit comments

Comments
 (0)