Skip to content

Commit 72ad54a

Browse files
add a Kubernetes External Metrics metrics provider
Co-authored-by: Johan Lore <johan.lore@decathlon.com> Co-authored-by: Maxime Véroone <maxime.veroone@decathlon.com> Signed-off-by: Johan Lore <johan.lore@decathlon.com>
1 parent 3a27fd1 commit 72ad54a

File tree

9 files changed

+378
-12
lines changed

9 files changed

+378
-12
lines changed

artifacts/flagger/crd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,7 @@ spec:
13441344
- prometheus
13451345
- influxdb
13461346
- datadog
1347+
- externalmetrics
13471348
- stackdriver
13481349
- cloudwatch
13491350
- newrelic

charts/flagger/crds/crd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,7 @@ spec:
13441344
- prometheus
13451345
- influxdb
13461346
- datadog
1347+
- externalmetrics
13471348
- stackdriver
13481349
- cloudwatch
13491350
- newrelic

charts/flagger/templates/rbac.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,14 @@ rules:
289289
- revisions
290290
verbs:
291291
- get
292+
- apiGroups:
293+
- external.metrics.k8s.io
294+
resources:
295+
- externalmetrics
296+
verbs:
297+
- get
298+
- watch
299+
- list
292300
---
293301
apiVersion: rbac.authorization.k8s.io/v1
294302
kind: ClusterRoleBinding

go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ require (
2424
google.golang.org/grpc v1.76.0
2525
google.golang.org/protobuf v1.36.10
2626
gopkg.in/h2non/gock.v1 v1.1.2
27-
k8s.io/api v0.34.1
28-
k8s.io/apimachinery v0.34.1
29-
k8s.io/client-go v0.34.1
30-
k8s.io/code-generator v0.34.1
27+
k8s.io/api v0.34.2
28+
k8s.io/apimachinery v0.34.2
29+
k8s.io/client-go v0.34.2
30+
k8s.io/code-generator v0.34.2
3131
k8s.io/klog/v2 v2.130.1
32+
k8s.io/metrics v0.34.2
3233
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
3334
knative.dev/serving v0.46.6
3435
)

go.sum

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -275,20 +275,22 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
275275
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
276276
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
277277
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
278-
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
279-
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
280-
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
281-
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
282-
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
283-
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
284-
k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc=
285-
k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg=
278+
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
279+
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
280+
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
281+
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
282+
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
283+
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
284+
k8s.io/code-generator v0.34.2 h1:9bG6jTxmsU3HXE5BNYJTC8AZ1D6hVVfkm8yYSkdkGY0=
285+
k8s.io/code-generator v0.34.2/go.mod h1:dnDDEd6S/z4uZ+PG1aE58ySCi/lR4+qT3a4DddE4/2I=
286286
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q=
287287
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
288288
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
289289
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
290290
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
291291
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
292+
k8s.io/metrics v0.34.2 h1:zao91FNDVPRGIiHLO2vqqe21zZVPien1goyzn0hsz90=
293+
k8s.io/metrics v0.34.2/go.mod h1:Ydulln+8uZZctUM8yrUQX4rfq/Ay6UzsuXf24QJ37Vc=
292294
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
293295
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
294296
knative.dev/networking v0.0.0-20250902160145-7dad473f6351 h1:Gv/UqbN0AK+ORoT5e2Kg+3+uMW/y9CCdhpXKxYaVV6k=

kustomize/base/flagger/crd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,7 @@ spec:
13441344
- prometheus
13451345
- influxdb
13461346
- datadog
1347+
- externalmetrics
13471348
- stackdriver
13481349
- cloudwatch
13491350
- newrelic
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
Copyright 2020 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package providers
18+
19+
import (
20+
"context"
21+
"crypto/tls"
22+
"encoding/json"
23+
"fmt"
24+
"io"
25+
"os"
26+
"net"
27+
"net/http"
28+
"net/url"
29+
"time"
30+
31+
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
32+
"k8s.io/metrics/pkg/apis/external_metrics"
33+
)
34+
35+
const (
36+
metricServiceEndpointPath = "/apis/external.metrics.k8s.io/v1beta1"
37+
namespacesPath = "/namespaces/"
38+
39+
authorizationHeaderKey = "Authorization"
40+
applicationBearerToken = "token"
41+
)
42+
43+
// ExternalMetricsProvider executes datadog queries
44+
type ExternalMetricsProvider struct {
45+
metricServiceEndpoint string
46+
bearerToken string // Find out if we can get authoritative answer that this is the standard
47+
48+
timeout time.Duration
49+
client *http.Client
50+
}
51+
52+
// NewExternalMetricsProvider takes a canary spec, a provider spec, and
53+
// returns a client ready to execute queries against the Service
54+
func NewExternalMetricsProvider(metricInterval string,
55+
provider flaggerv1.MetricTemplateProvider,
56+
credentials map[string][]byte) (*ExternalMetricsProvider, error) {
57+
58+
if provider.Address == "" {
59+
return nil, fmt.Errorf("the Url of the external metric service must be provided")
60+
}
61+
62+
externalMetrics := ExternalMetricsProvider{
63+
metricServiceEndpoint: fmt.Sprintf("%s%s", provider.Address, metricServiceEndpointPath),
64+
timeout: 5 * time.Second,
65+
client: http.DefaultClient,
66+
}
67+
68+
if provider.InsecureSkipVerify {
69+
t := http.DefaultTransport.(*http.Transport).Clone()
70+
t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
71+
externalMetrics.client = &http.Client{Transport: t}
72+
}
73+
74+
if b, ok := credentials[applicationBearerToken]; ok {
75+
externalMetrics.bearerToken = string(b)
76+
} else {
77+
// Read service account token from volume mount
78+
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
79+
if err != nil {
80+
return nil, fmt.Errorf("error reading service account token: %w", err)
81+
}
82+
if len(token) == 0 {
83+
return nil, fmt.Errorf("pod's service account token is empty")
84+
}
85+
externalMetrics.bearerToken = string(token)
86+
}
87+
88+
return &externalMetrics, nil
89+
}
90+
91+
// RunQuery retrieves the ExternalMetricValue from the ExternalMetricsProvider.metricServiceUrl
92+
// and returns the first result as a float64
93+
func (p *ExternalMetricsProvider) RunQuery(query string) (float64, error) {
94+
95+
metricsQueryUrl := fmt.Sprintf("%s%s%s", p.metricServiceEndpoint, namespacesPath, query)
96+
//TODO add labelSelector as queryString (in the docs of this provider as it's embedded in the query string)
97+
98+
req, err := http.NewRequest("GET", metricsQueryUrl, nil)
99+
if err != nil {
100+
return 0, fmt.Errorf("error http.NewRequest: %w", err)
101+
}
102+
if p.bearerToken != "" {
103+
req.Header.Add(authorizationHeaderKey, fmt.Sprintf("Bearer %s", p.bearerToken))
104+
}
105+
106+
ctx, cancel := context.WithTimeout(req.Context(), p.timeout)
107+
defer cancel()
108+
r, err := p.client.Do(req.WithContext(ctx))
109+
if err != nil {
110+
return 0, fmt.Errorf("request failed: %w", err)
111+
}
112+
113+
defer r.Body.Close()
114+
b, err := io.ReadAll(r.Body)
115+
if err != nil {
116+
return 0, fmt.Errorf("error reading body: %w", err)
117+
}
118+
119+
if r.StatusCode != http.StatusOK {
120+
return 0, fmt.Errorf("error response: %s: %w", string(b), err)
121+
}
122+
123+
var res external_metrics.ExternalMetricValueList
124+
if err := json.Unmarshal(b, &res); err != nil {
125+
return 0, fmt.Errorf("error unmarshaling result: %w, '%s'", err, string(b))
126+
}
127+
128+
if len(res.Items) < 1 {
129+
return 0, fmt.Errorf("invalid response: %s: %w", string(b), ErrNoValuesFound)
130+
}
131+
132+
vs := res.Items[0].Value.AsApproximateFloat64()
133+
134+
return vs, nil
135+
}
136+
137+
// IsOnline will only check the TCP endpoint reachability,
138+
// given that external metric servers don't have a common health check endpoint defined
139+
func (p *ExternalMetricsProvider) IsOnline() (bool, error) {
140+
var d net.Dialer
141+
142+
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
143+
defer cancel()
144+
145+
metricServiceUrl, err := url.Parse(p.metricServiceEndpoint)
146+
if err != nil {
147+
return false, fmt.Errorf("error parsing metric service url: %w", err)
148+
}
149+
150+
conn, err := d.DialContext(ctx, "tcp", metricServiceUrl.Host)
151+
defer conn.Close()
152+
if err != nil {
153+
return false, fmt.Errorf("connection failed: %w", err)
154+
}
155+
return true, err
156+
}

0 commit comments

Comments
 (0)