Skip to content

Commit 3757b5a

Browse files
Get token from bound service account if not provided by the metrictemplate
Add test for TLS verification and some variables in test file Amend CRD with provider and ClusterRole with external-metrics API feat(externalmetrics): implement ExternalMetricsProvider for querying external metrics 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 3757b5a

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)