1
16
17 package envtest
18
19 import (
20 "context"
21 "fmt"
22 "os"
23 "strings"
24 "time"
25
26 corev1 "k8s.io/api/core/v1"
27 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/apimachinery/pkg/types"
30 "k8s.io/apimachinery/pkg/util/wait"
31 "k8s.io/client-go/kubernetes/scheme"
32 "k8s.io/client-go/rest"
33 "sigs.k8s.io/controller-runtime/pkg/client"
34
35 "sigs.k8s.io/controller-runtime/pkg/client/config"
36 logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
37 "sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane"
38 "sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
39 )
40
41 var log = logf.RuntimeLog.WithName("test-env")
42
43
54 const (
55 envUseExistingCluster = "USE_EXISTING_CLUSTER"
56 envStartTimeout = "KUBEBUILDER_CONTROLPLANE_START_TIMEOUT"
57 envStopTimeout = "KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT"
58 envAttachOutput = "KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT"
59 StartTimeout = 60
60 StopTimeout = 60
61
62 defaultKubebuilderControlPlaneStartTimeout = 20 * time.Second
63 defaultKubebuilderControlPlaneStopTimeout = 20 * time.Second
64 )
65
66
67 type (
68
69 ControlPlane = controlplane.ControlPlane
70
71
72 APIServer = controlplane.APIServer
73
74
75 Etcd = controlplane.Etcd
76
77
78 User = controlplane.User
79
80
81 AuthenticatedUser = controlplane.AuthenticatedUser
82
83
84 ListenAddr = process.ListenAddr
85
86
87
88 SecureServing = controlplane.SecureServing
89
90
91
92 Authn = controlplane.Authn
93
94
95 Arguments = process.Arguments
96
97
98 Arg = process.Arg
99 )
100
101 var (
102
103
104
105
106 EmptyArguments = process.EmptyArguments
107 )
108
109
110
111 type Environment struct {
112
113 ControlPlane controlplane.ControlPlane
114
115
116
117
118
119
120
121
122 Scheme *runtime.Scheme
123
124
125
126
127 Config *rest.Config
128
129
130 CRDInstallOptions CRDInstallOptions
131
132
133 WebhookInstallOptions WebhookInstallOptions
134
135
136
137
138 ErrorIfCRDPathMissing bool
139
140
141
142
143 CRDs []*apiextensionsv1.CustomResourceDefinition
144
145
146
147
148 CRDDirectoryPaths []string
149
150
151
152 BinaryAssetsDirectory string
153
154
155
156
157 UseExistingCluster *bool
158
159
160
161
162 ControlPlaneStartTimeout time.Duration
163
164
165
166
167 ControlPlaneStopTimeout time.Duration
168
169
170
171
172 AttachControlPlaneOutput bool
173 }
174
175
176
177
178 func (te *Environment) Stop() error {
179 if te.CRDInstallOptions.CleanUpAfterUse {
180 if err := UninstallCRDs(te.Config, te.CRDInstallOptions); err != nil {
181 return err
182 }
183 }
184
185 if err := te.WebhookInstallOptions.Cleanup(); err != nil {
186 return err
187 }
188
189 if te.useExistingCluster() {
190 return nil
191 }
192
193 return te.ControlPlane.Stop()
194 }
195
196
197 func (te *Environment) Start() (*rest.Config, error) {
198 if te.useExistingCluster() {
199 log.V(1).Info("using existing cluster")
200 if te.Config == nil {
201
202
203 log.V(1).Info("automatically acquiring client configuration")
204
205 var err error
206 te.Config, err = config.GetConfig()
207 if err != nil {
208 return nil, fmt.Errorf("unable to get configuration for existing cluster: %w", err)
209 }
210 }
211 } else {
212 apiServer := te.ControlPlane.GetAPIServer()
213
214 if te.ControlPlane.Etcd == nil {
215 te.ControlPlane.Etcd = &controlplane.Etcd{}
216 }
217
218 if os.Getenv(envAttachOutput) == "true" {
219 te.AttachControlPlaneOutput = true
220 }
221 if te.AttachControlPlaneOutput {
222 if apiServer.Out == nil {
223 apiServer.Out = os.Stdout
224 }
225 if apiServer.Err == nil {
226 apiServer.Err = os.Stderr
227 }
228 if te.ControlPlane.Etcd.Out == nil {
229 te.ControlPlane.Etcd.Out = os.Stdout
230 }
231 if te.ControlPlane.Etcd.Err == nil {
232 te.ControlPlane.Etcd.Err = os.Stderr
233 }
234 }
235
236 apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory)
237 te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory)
238 te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory)
239
240 if err := te.defaultTimeouts(); err != nil {
241 return nil, fmt.Errorf("failed to default controlplane timeouts: %w", err)
242 }
243 te.ControlPlane.Etcd.StartTimeout = te.ControlPlaneStartTimeout
244 te.ControlPlane.Etcd.StopTimeout = te.ControlPlaneStopTimeout
245 apiServer.StartTimeout = te.ControlPlaneStartTimeout
246 apiServer.StopTimeout = te.ControlPlaneStopTimeout
247
248 log.V(1).Info("starting control plane")
249 if err := te.startControlPlane(); err != nil {
250 return nil, fmt.Errorf("unable to start control plane itself: %w", err)
251 }
252
253
254 baseConfig := &rest.Config{
255
256 QPS: 1000.0,
257 Burst: 2000.0,
258 }
259
260 adminInfo := User{Name: "admin", Groups: []string{"system:masters"}}
261 adminUser, err := te.ControlPlane.AddUser(adminInfo, baseConfig)
262 if err != nil {
263 return te.Config, fmt.Errorf("unable to provision admin user: %w", err)
264 }
265 te.Config = adminUser.Config()
266 }
267
268
269 if te.Scheme == nil {
270 te.Scheme = scheme.Scheme
271 }
272
273
274
275 if err := te.waitForDefaultNamespace(te.Config); err != nil {
276 return nil, fmt.Errorf("default namespace didn't register within deadline: %w", err)
277 }
278
279
280
281 if err := te.WebhookInstallOptions.PrepWithoutInstalling(); err != nil {
282 return nil, err
283 }
284
285 log.V(1).Info("installing CRDs")
286 if te.CRDInstallOptions.Scheme == nil {
287 te.CRDInstallOptions.Scheme = te.Scheme
288 }
289 te.CRDInstallOptions.CRDs = mergeCRDs(te.CRDInstallOptions.CRDs, te.CRDs)
290 te.CRDInstallOptions.Paths = mergePaths(te.CRDInstallOptions.Paths, te.CRDDirectoryPaths)
291 te.CRDInstallOptions.ErrorIfPathMissing = te.ErrorIfCRDPathMissing
292 te.CRDInstallOptions.WebhookOptions = te.WebhookInstallOptions
293 crds, err := InstallCRDs(te.Config, te.CRDInstallOptions)
294 if err != nil {
295 return te.Config, fmt.Errorf("unable to install CRDs onto control plane: %w", err)
296 }
297 te.CRDs = crds
298
299 log.V(1).Info("installing webhooks")
300 if err := te.WebhookInstallOptions.Install(te.Config); err != nil {
301 return nil, fmt.Errorf("unable to install webhooks onto control plane: %w", err)
302 }
303 return te.Config, nil
304 }
305
306
307
308
309
310
311
312
313
314
315 func (te *Environment) AddUser(user User, baseConfig *rest.Config) (*AuthenticatedUser, error) {
316 return te.ControlPlane.AddUser(user, baseConfig)
317 }
318
319 func (te *Environment) startControlPlane() error {
320 numTries, maxRetries := 0, 5
321 var err error
322 for ; numTries < maxRetries; numTries++ {
323
324 err = te.ControlPlane.Start()
325 if err == nil {
326 break
327 }
328 log.Error(err, "unable to start the controlplane", "tries", numTries)
329 }
330 if numTries == maxRetries {
331 return fmt.Errorf("failed to start the controlplane. retried %d times: %w", numTries, err)
332 }
333 return nil
334 }
335
336 func (te *Environment) waitForDefaultNamespace(config *rest.Config) error {
337 cs, err := client.New(config, client.Options{})
338 if err != nil {
339 return fmt.Errorf("unable to create client: %w", err)
340 }
341
342 return wait.PollUntilContextTimeout(context.TODO(), time.Millisecond*50, time.Second*5, true, func(ctx context.Context) (bool, error) {
343 if err = cs.Get(ctx, types.NamespacedName{Name: "default"}, &corev1.Namespace{}); err != nil {
344 return false, nil
345 }
346 return true, nil
347 })
348 }
349
350 func (te *Environment) defaultTimeouts() error {
351 var err error
352 if te.ControlPlaneStartTimeout == 0 {
353 if envVal := os.Getenv(envStartTimeout); envVal != "" {
354 te.ControlPlaneStartTimeout, err = time.ParseDuration(envVal)
355 if err != nil {
356 return err
357 }
358 } else {
359 te.ControlPlaneStartTimeout = defaultKubebuilderControlPlaneStartTimeout
360 }
361 }
362
363 if te.ControlPlaneStopTimeout == 0 {
364 if envVal := os.Getenv(envStopTimeout); envVal != "" {
365 te.ControlPlaneStopTimeout, err = time.ParseDuration(envVal)
366 if err != nil {
367 return err
368 }
369 } else {
370 te.ControlPlaneStopTimeout = defaultKubebuilderControlPlaneStopTimeout
371 }
372 }
373 return nil
374 }
375
376 func (te *Environment) useExistingCluster() bool {
377 if te.UseExistingCluster == nil {
378 return strings.ToLower(os.Getenv(envUseExistingCluster)) == "true"
379 }
380 return *te.UseExistingCluster
381 }
382
383
384
385
386
387 var DefaultKubeAPIServerFlags = controlplane.APIServerDefaultArgs
388
View as plain text