1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package testcontroller
16
17 import (
18 "context"
19 "errors"
20 "fmt"
21 "log"
22 "math/rand"
23 "regexp"
24 "strings"
25 "sync"
26 "testing"
27 "time"
28
29 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/kccmanager/nocache"
30 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdloader"
31 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
32 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
33 testgcp "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/gcp"
34 cnrmwebhook "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook"
35
36 corev1 "k8s.io/api/core/v1"
37 apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
38 apierrors "k8s.io/apimachinery/pkg/api/errors"
39 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
40 "k8s.io/apimachinery/pkg/types"
41 "k8s.io/klog/v2"
42 "sigs.k8s.io/controller-runtime/pkg/client"
43 "sigs.k8s.io/controller-runtime/pkg/envtest"
44 "sigs.k8s.io/controller-runtime/pkg/manager"
45 "sigs.k8s.io/controller-runtime/pkg/reconcile"
46 "sigs.k8s.io/controller-runtime/pkg/webhook"
47 )
48
49 const (
50
51 transientErrorsMaxRetries = 5
52
53 transientErrorsRetryInterval = 5 * time.Second
54 )
55
56
57
58 func StartTestManagerInstance(env *envtest.Environment, testType test.TestType, whCfgs []cnrmwebhook.WebhookConfig) (manager.Manager, func()) {
59 mgr, stopFunc, err := startTestManager(env, testType, whCfgs)
60 if err != nil {
61 log.Fatal(err)
62 }
63 return mgr, stopFunc
64 }
65
66 func startTestManager(env *envtest.Environment, testType test.TestType, whCfgs []cnrmwebhook.WebhookConfig) (manager.Manager, func(), error) {
67 mgr, err := manager.New(env.Config, manager.Options{
68 Port: env.WebhookInstallOptions.LocalServingPort,
69 Host: env.WebhookInstallOptions.LocalServingHost,
70 CertDir: env.WebhookInstallOptions.LocalServingCertDir,
71
72 NewClient: nocache.NoCacheClientFunc,
73
74 MetricsBindAddress: "0",
75 })
76 if err != nil {
77 return nil, nil, fmt.Errorf("error creating manager: %v", err)
78 }
79 if testType == test.IntegrationTestType {
80 server := mgr.GetWebhookServer()
81 for _, cfg := range whCfgs {
82 server.Register(cfg.Path, &webhook.Admission{Handler: cfg.Handler})
83 }
84 }
85 stop := startMgr(mgr, log.Fatalf)
86 return mgr, stop, nil
87 }
88
89 func StartMgr(t *testing.T, mgr manager.Manager) func() {
90 return startMgr(mgr, t.Fatalf)
91 }
92
93 func startMgr(mgr manager.Manager, mgrStartErrHandler func(string, ...interface{})) func() {
94 ctx, cancel := context.WithCancel(context.TODO())
95
96
97 wg := sync.WaitGroup{}
98 wg.Add(1)
99 go func() {
100 defer wg.Done()
101 if err := mgr.Start(ctx); err != nil {
102 mgrStartErrHandler("unable to start manager: %v", err)
103 }
104 }()
105 stop := func() {
106
107
108 cancel()
109
110 wg.Wait()
111 }
112 return stop
113 }
114
115
116 func isTransientError(t *testing.T, err error) bool {
117 if err == nil {
118 return false
119 }
120
121
122 var chain []string
123 current := err
124 for {
125 chain = append(chain, fmt.Sprintf("[%T: %+v]", current, current))
126 current = errors.Unwrap(current)
127 if current == nil {
128 break
129 }
130 }
131
132 errorMessage := err.Error()
133
134
135
136
137
138
139
140
141
142 if strings.Contains(errorMessage, "The caller does not have permission") {
143 t.Logf("permission error found; considered transient; chain is %v", chain)
144 return true
145 }
146
147
148
149
150
151
152
153 if strings.Contains(errorMessage, "An internal exception occurred") {
154 t.Logf("internal error found; considered transient; chain is %v", chain)
155 return true
156 }
157
158
159
160
161
162
163
164 if strings.Contains(errorMessage, "is not ready") {
165 t.Logf("internal error found; considered transient; chain is %v", chain)
166 return true
167 }
168
169
170
171
172
173
174 if strings.Contains(errorMessage, "missing permission on") {
175 t.Logf("internal error found; considered transient; chain is %v", chain)
176 return true
177 }
178
179
180
181
182
183
184
185 if strings.Contains(errorMessage, "Hook call/poll failed for service") {
186 t.Logf("internal error found; considered transient; chain is %v", chain)
187 return true
188 }
189
190 t.Logf("error was not considered transient; chain is %v", chain)
191 return false
192 }
193
194
195 func RunReconcilerAssertResults(t *testing.T, reconciler reconcile.Reconciler, objectMeta v1.ObjectMeta,
196 expectedResult reconcile.Result, expectedErrorRegex *regexp.Regexp) {
197 attempt := 0
198 tryAgain:
199 attempt++
200 t.Helper()
201 reconcileRequest := reconcile.Request{NamespacedName: k8s.GetNamespacedName(objectMeta.GetObjectMeta())}
202 result, err := reconciler.Reconcile(context.Background(), reconcileRequest)
203
204
205 if err != nil {
206 if isTransientError(t, err) {
207 if attempt < transientErrorsMaxRetries {
208 t.Logf("detected transient error, will retry: %v", err)
209 time.Sleep(transientErrorsRetryInterval)
210 goto tryAgain
211 } else {
212 t.Logf("detected transient error, but maximum number of retries reached: %v", err)
213 }
214 }
215 }
216
217 if expectedErrorRegex == nil {
218 if err != nil {
219 t.Fatalf("reconcile returned unexpected error: %v", err)
220 }
221 } else {
222 if err == nil || !expectedErrorRegex.MatchString(err.Error()) {
223 t.Fatalf("error '%v' does not match regex '%v'", err, expectedErrorRegex)
224 }
225 }
226 if !(requeueEqualAndRequeueAfterWithinBoundsOfMean(result, expectedResult)) {
227 t.Fatalf("reconcile result mismatch: got '%v', want within %v of '%v'", result, k8s.MeanReconcileReenqueuePeriod/2, expectedResult)
228 }
229 }
230
231 func GetCRDForKind(t *testing.T, kubeClient client.Client, kind string) *apiextensions.CustomResourceDefinition {
232 t.Helper()
233 c, err := crdloader.GetCRDForKind(kind)
234 if err != nil {
235 t.Fatal(err)
236 }
237 return c
238 }
239
240 func SetupNamespaceForDefaultProject(t *testing.T, c client.Client, name string) {
241 projectID := testgcp.GetDefaultProjectID(t)
242 SetupNamespaceForProject(t, c, name, projectID)
243 }
244
245 func SetupNamespaceForProject(t *testing.T, c client.Client, name, projectID string) {
246 EnsureNamespaceExistsT(t, c, name)
247 EnsureNamespaceHasProjectIDAnnotation(t, c, name, projectID)
248 }
249
250 func EnsureNamespaceExists(c client.Client, name string) error {
251 ns := &corev1.Namespace{}
252 ns.SetName(name)
253 if err := c.Create(context.Background(), ns); err != nil {
254 if !apierrors.IsAlreadyExists(err) {
255 return fmt.Errorf("error creating namespace %v: %v", name, err)
256 }
257 }
258 return nil
259 }
260
261 func EnsureNamespaceExistsT(t *testing.T, c client.Client, name string) {
262 t.Helper()
263 if err := EnsureNamespaceExists(c, name); err != nil {
264 t.Fatal(err)
265 }
266 }
267
268 func EnsureNamespaceHasProjectIDAnnotation(t *testing.T, c client.Client, namespaceName, projectId string) {
269 t.Helper()
270 err := createNamespaceProjectIdAnnotation(context.TODO(), c, namespaceName, projectId)
271 if err != nil {
272 t.Fatal(err)
273 }
274 }
275
276 func createNamespaceProjectIdAnnotation(ctx context.Context, c client.Client, namespaceName, projectId string) error {
277 tryAgain:
278 attempt := 0
279 var ns corev1.Namespace
280 if err := c.Get(ctx, types.NamespacedName{Name: namespaceName}, &ns); err != nil {
281 return fmt.Errorf("error getting namespace %q: %w", namespaceName, err)
282 }
283 if val, ok := k8s.GetAnnotation(k8s.ProjectIDAnnotation, &ns); ok {
284 if val == projectId {
285 klog.Infof("namespace %q already has project id annotation value %q", namespaceName, projectId)
286 return nil
287 } else {
288 return fmt.Errorf("cannot set project id annotatation value to %q: the annotation already contained a value of %q",
289 projectId, val)
290 }
291 }
292 k8s.SetAnnotation(k8s.ProjectIDAnnotation, projectId, &ns)
293 err := c.Update(ctx, &ns)
294 if err != nil {
295 if apierrors.IsConflict(err) {
296 attempt++
297 if attempt < 10 {
298 klog.Warningf("detected concurrent modification error updating namespace %q, will retry", namespaceName)
299 time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
300 goto tryAgain
301 }
302 }
303 return fmt.Errorf("error setting project id on namespace %q: %w", namespaceName, err)
304 }
305 return nil
306 }
307
308 func requeueEqualAndRequeueAfterWithinBoundsOfMean(result reconcile.Result, expectedResult reconcile.Result) bool {
309 requeueEqual := result.Requeue == expectedResult.Requeue
310 lowerBound := expectedResult.RequeueAfter / 2
311 upperBound := expectedResult.RequeueAfter / 2 * 3
312 return requeueEqual && (result.RequeueAfter >= lowerBound && result.RequeueAfter < upperBound || result.RequeueAfter == 0 && expectedResult.RequeueAfter == 0)
313 }
314
View as plain text