1
16
17 package filters
18
19 import (
20 "context"
21 "crypto/tls"
22 "errors"
23 "fmt"
24 "io"
25 "net/http"
26 "reflect"
27 "time"
28
29 . "github.com/onsi/ginkgo/v2"
30 . "github.com/onsi/gomega"
31 "github.com/prometheus/client_golang/prometheus"
32 authenticationv1 "k8s.io/api/authentication/v1"
33 corev1 "k8s.io/api/core/v1"
34 rbacv1 "k8s.io/api/rbac/v1"
35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36 "k8s.io/client-go/rest"
37 "k8s.io/utils/ptr"
38 "sigs.k8s.io/controller-runtime/pkg/client"
39 "sigs.k8s.io/controller-runtime/pkg/manager"
40 "sigs.k8s.io/controller-runtime/pkg/metrics"
41 metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
42 )
43
44 var _ = Describe("manger.Manager", func() {
45 Describe("Start", func() {
46 Context("should start serving metrics with https and authn/authz", func() {
47 var srv metricsserver.Server
48 var defaultServer metricsDefaultServer
49 var opts manager.Options
50 var httpClient *http.Client
51
52 BeforeEach(func() {
53 srv = nil
54 newMetricsServer := func(options metricsserver.Options, config *rest.Config, httpClient *http.Client) (metricsserver.Server, error) {
55 var err error
56 srv, err = metricsserver.NewServer(options, config, httpClient)
57 if srv != nil {
58 defaultServer = srv.(metricsDefaultServer)
59 }
60 return srv, err
61 }
62 opts = manager.Options{
63 Metrics: metricsserver.Options{
64 BindAddress: ":0",
65 SecureServing: true,
66 FilterProvider: WithAuthenticationAndAuthorization,
67 },
68 }
69 v := reflect.ValueOf(&opts).Elem()
70 newMetricsField := v.FieldByName("newMetricsServer")
71 reflect.NewAt(newMetricsField.Type(), newMetricsField.Addr().UnsafePointer()).
72 Elem().
73 Set(reflect.ValueOf(newMetricsServer))
74 httpClient = &http.Client{Transport: &http.Transport{
75 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
76 }}
77 })
78
79 It("should serve metrics in its registry", func() {
80 one := prometheus.NewCounter(prometheus.CounterOpts{
81 Name: "test_one",
82 Help: "test metric for testing",
83 })
84 one.Inc()
85 err := metrics.Registry.Register(one)
86 Expect(err).NotTo(HaveOccurred())
87
88 m, err := manager.New(cfg, opts)
89 Expect(err).NotTo(HaveOccurred())
90
91 ctx, cancel := context.WithCancel(context.Background())
92 defer cancel()
93 go func() {
94 defer GinkgoRecover()
95 Expect(m.Start(ctx)).NotTo(HaveOccurred())
96 }()
97 <-m.Elected()
98
99
100 Eventually(func() string { return defaultServer.GetBindAddr() }, 10*time.Second).ShouldNot(BeEmpty())
101
102
103 token, cleanup, err := setupServiceAccountForURL(ctx, m.GetClient(), "/metrics")
104 defer cleanup()
105 Expect(err).ToNot(HaveOccurred())
106
107
108 metricsEndpoint := fmt.Sprintf("https://%s/metrics", defaultServer.GetBindAddr())
109 req, err := http.NewRequest("GET", metricsEndpoint, nil)
110 Expect(err).NotTo(HaveOccurred())
111 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
112 resp, err := httpClient.Do(req)
113 Expect(err).NotTo(HaveOccurred())
114 defer resp.Body.Close()
115
116 Expect(resp.StatusCode).To(Equal(200))
117
118 data, err := io.ReadAll(resp.Body)
119 Expect(err).NotTo(HaveOccurred())
120 Expect(string(data)).To(ContainSubstring("%s\n%s\n%s\n",
121 `# HELP test_one test metric for testing`,
122 `# TYPE test_one counter`,
123 `test_one 1`,
124 ))
125
126
127 ok := metrics.Registry.Unregister(one)
128 Expect(ok).To(BeTrue())
129 })
130
131 It("should serve extra endpoints", func() {
132 opts.Metrics.ExtraHandlers = map[string]http.Handler{
133 "/debug": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
134 _, _ = w.Write([]byte("Some debug info"))
135 }),
136 }
137 m, err := manager.New(cfg, opts)
138 Expect(err).NotTo(HaveOccurred())
139
140 ctx, cancel := context.WithCancel(context.Background())
141 defer cancel()
142 go func() {
143 defer GinkgoRecover()
144 Expect(m.Start(ctx)).NotTo(HaveOccurred())
145 }()
146 <-m.Elected()
147
148
149 Eventually(func() string { return defaultServer.GetBindAddr() }, 10*time.Second).ShouldNot(BeEmpty())
150
151
152 token, cleanup, err := setupServiceAccountForURL(ctx, m.GetClient(), "/debug")
153 defer cleanup()
154 Expect(err).ToNot(HaveOccurred())
155
156
157 endpoint := fmt.Sprintf("https://%s/debug", defaultServer.GetBindAddr())
158 req, err := http.NewRequest("GET", endpoint, nil)
159 Expect(err).NotTo(HaveOccurred())
160 resp, err := httpClient.Do(req)
161 Expect(err).NotTo(HaveOccurred())
162 defer resp.Body.Close()
163
164 Expect(resp.StatusCode).To(Equal(401))
165 body, err := io.ReadAll(resp.Body)
166 Expect(err).NotTo(HaveOccurred())
167 Expect(string(body)).To(ContainSubstring("Unauthorized"))
168
169
170 req, err = http.NewRequest("PUT", endpoint, nil)
171 Expect(err).NotTo(HaveOccurred())
172 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
173 resp, err = httpClient.Do(req)
174 Expect(err).NotTo(HaveOccurred())
175 defer resp.Body.Close()
176
177 Expect(resp.StatusCode).To(Equal(200))
178 body, err = io.ReadAll(resp.Body)
179 Expect(err).NotTo(HaveOccurred())
180 Expect(string(body)).To(Equal("Some debug info"))
181
182
183 metricsEndpoint := fmt.Sprintf("https://%s/metrics", defaultServer.GetBindAddr())
184 req, err = http.NewRequest("GET", metricsEndpoint, nil)
185 Expect(err).NotTo(HaveOccurred())
186 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
187 resp, err = httpClient.Do(req)
188 Expect(err).NotTo(HaveOccurred())
189 defer resp.Body.Close()
190 Expect(resp.StatusCode).To(Equal(403))
191 body, err = io.ReadAll(resp.Body)
192 Expect(err).NotTo(HaveOccurred())
193
194 Expect(string(body)).To(ContainSubstring("Authorization denied for user system:serviceaccount:default:metrics-test"))
195 })
196 })
197 })
198 })
199
200 type metricsDefaultServer interface {
201 GetBindAddr() string
202 }
203
204 func setupServiceAccountForURL(ctx context.Context, c client.Client, path string) (string, func(), error) {
205 createdObjects := []client.Object{}
206 cleanup := func() {
207 for _, obj := range createdObjects {
208 _ = c.Delete(ctx, obj)
209 }
210 }
211
212 sa := &corev1.ServiceAccount{
213 ObjectMeta: metav1.ObjectMeta{
214 Name: "metrics-test",
215 Namespace: metav1.NamespaceDefault,
216 },
217 }
218 if err := c.Create(ctx, sa); err != nil {
219 return "", cleanup, err
220 }
221 createdObjects = append(createdObjects, sa)
222
223 cr := &rbacv1.ClusterRole{
224 ObjectMeta: metav1.ObjectMeta{
225 Name: "metrics-test",
226 },
227 Rules: []rbacv1.PolicyRule{
228 {
229 Verbs: []string{"get", "put"},
230 NonResourceURLs: []string{path},
231 },
232 },
233 }
234 if err := c.Create(ctx, cr); err != nil {
235 return "", cleanup, err
236 }
237 createdObjects = append(createdObjects, cr)
238
239 crb := &rbacv1.ClusterRoleBinding{
240 ObjectMeta: metav1.ObjectMeta{
241 Name: "metrics-test",
242 },
243 Subjects: []rbacv1.Subject{
244 {
245 Kind: rbacv1.ServiceAccountKind,
246 Name: "metrics-test",
247 Namespace: metav1.NamespaceDefault,
248 },
249 },
250 RoleRef: rbacv1.RoleRef{
251 APIGroup: rbacv1.GroupName,
252 Kind: "ClusterRole",
253 Name: "metrics-test",
254 },
255 }
256 if err := c.Create(ctx, crb); err != nil {
257 return "", cleanup, err
258 }
259 createdObjects = append(createdObjects, crb)
260
261 tokenRequest := &authenticationv1.TokenRequest{
262 Spec: authenticationv1.TokenRequestSpec{
263 ExpirationSeconds: ptr.To(int64(2 * 60 * 60)),
264 },
265 }
266 if err := c.SubResource("token").Create(ctx, sa, tokenRequest); err != nil {
267 return "", cleanup, err
268 }
269
270 if tokenRequest.Status.Token == "" {
271 return "", cleanup, errors.New("failed to get ServiceAccount token: token should not be empty")
272 }
273
274 return tokenRequest.Status.Token, cleanup, nil
275 }
276
View as plain text