1 package dennis
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "testing"
9 "time"
10
11 compute "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/compute/v1beta1"
12 dns "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/dns/v1beta1"
13 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1"
14 "github.com/stretchr/testify/suite"
15 v1 "k8s.io/api/core/v1"
16 "k8s.io/apimachinery/pkg/api/errors"
17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18 "k8s.io/apimachinery/pkg/types"
19 "k8s.io/apimachinery/pkg/util/yaml"
20
21 "edge-infra.dev/pkg/k8s/konfigkonnector/apis/meta"
22 "edge-infra.dev/pkg/k8s/runtime/controller"
23 "edge-infra.dev/test"
24 "edge-infra.dev/test/framework"
25 "edge-infra.dev/test/framework/gcp"
26 "edge-infra.dev/test/framework/integration"
27 "edge-infra.dev/test/framework/k8s"
28 "edge-infra.dev/test/framework/k8s/envtest"
29 )
30
31 func TestMain(m *testing.M) {
32
33
34 framework.HandleFlags()
35 os.Exit(m.Run())
36 }
37
38 type Suite struct {
39 *framework.Framework
40 *k8s.K8s
41 ctx context.Context
42 timeout time.Duration
43 tick time.Duration
44 projectID string
45 dnsProjectID string
46 domain string
47 zone string
48 }
49
50 func TestDNSRecordController(t *testing.T) {
51 testEnv := envtest.Setup()
52 mgr, _, err := create(controller.WithCfg(testEnv.Config), controller.WithMetricsAddress("0"))
53 test.NoError(err)
54
55 k := k8s.New(testEnv.Config, k8s.WithCtrlManager(mgr), k8s.WithKonfigKonnector())
56
57 f := framework.New("dennis").
58 Component("dennis").
59 Register(k)
60
61 s := &Suite{
62 Framework: f,
63 K8s: k,
64 ctx: context.Background(),
65 domain: "my-domain.edge-infra.dev",
66 projectID: "ret-edge-foo-fighters",
67 dnsProjectID: "ret-edge-dennys",
68 zone: "edge-infra",
69 }
70
71 if integration.IsIntegrationTest() {
72 s.projectID = gcp.GCloud.ProjectID
73 s.dnsProjectID = gcp.GCloud.ProjectID
74 s.timeout = k8s.Timeouts.DefaultTimeout
75 s.tick = k8s.Timeouts.Tick
76 } else {
77 s.timeout = time.Second * 1
78 s.tick = time.Millisecond * 10
79 }
80
81 suite.Run(t, s)
82
83 t.Cleanup(func() {
84 f.NoError(testEnv.Stop())
85 })
86 }
87
88 func (s *Suite) TestDNSRecordCreation_SameNamespace() {
89 addy := s.createAddy("same-namespace", true)
90
91 s.NoError(s.Client.Create(s.ctx, addy))
92
93 d := &dns.DNSRecordSet{}
94 s.Eventually(func() bool {
95 err := s.Client.Get(s.ctx, types.NamespacedName{
96 Name: addy.ObjectMeta.Name,
97 Namespace: s.Namespace,
98 }, d)
99 return !errors.IsNotFound(err)
100 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
101 s.Equal(s.domain, d.Spec.Name)
102 s.Equal(s.zone, d.Spec.ManagedZoneRef.Name)
103 id, err := meta.GetProjectAnnotation(d.ObjectMeta)
104 s.NoError(err)
105 s.Equal(s.dnsProjectID, id)
106 s.Equal("", d.Spec.ManagedZoneRef.Namespace)
107 }
108
109 func (s *Suite) TestDNSRecordCreation_DifferentNamespace() {
110 zoneNS := "edge-infra-namespace"
111 zone := fmt.Sprintf("%s/%s", zoneNS, s.zone)
112 addy := s.createAddy("diff-namespace", true)
113 addy.ObjectMeta.Annotations[ManagedZoneAnnotation] = zone
114
115 s.NoError(s.Client.Create(s.ctx, addy))
116
117 d := &dns.DNSRecordSet{}
118 s.Eventually(func() bool {
119 err := s.Client.Get(s.ctx, types.NamespacedName{
120 Name: addy.ObjectMeta.Name,
121 Namespace: s.Namespace,
122 }, d)
123 return !errors.IsNotFound(err)
124 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
125 s.Equal(s.domain, d.Spec.Name)
126 s.Equal(s.zone, d.Spec.ManagedZoneRef.Name)
127 id, err := meta.GetProjectAnnotation(d.ObjectMeta)
128 s.NoError(err)
129 s.Equal(s.dnsProjectID, id)
130 s.Equal(zoneNS, d.Spec.ManagedZoneRef.Namespace)
131 }
132
133 func (s *Suite) TestDNSRecordNotCreated() {
134 addy := s.createAddy("no-record", false)
135 s.NoError(s.Client.Create(s.ctx, addy))
136 s.Never(func() bool {
137 d := &dns.DNSRecordSet{}
138 err := s.Client.Get(s.ctx, types.NamespacedName{
139 Name: addy.ObjectMeta.Name,
140 Namespace: s.Namespace,
141 }, d)
142 return !errors.IsNotFound(err)
143 }, s.timeout, s.tick, "DNSRecordSet created when it shouldn't have been")
144 }
145
146 func (s *Suite) TestDNSRecordSetGarbageCollection() {
147 integration.SkipIfNot(s.Framework)
148 addy := s.createAddy("to-be-deleted", true)
149
150 s.NoError(s.Client.Create(s.ctx, addy))
151
152 s.Eventually(func() bool {
153 d := &dns.DNSRecordSet{}
154 err := s.Client.Get(s.ctx, types.NamespacedName{
155 Name: addy.ObjectMeta.Name,
156 Namespace: s.Namespace,
157 }, d)
158 return !errors.IsNotFound(err)
159 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
160
161 s.NoError(s.Client.Delete(s.ctx, addy))
162
163 s.Eventually(func() bool {
164 d := &dns.DNSRecordSet{}
165 err := s.Client.Get(s.ctx, types.NamespacedName{
166 Name: addy.ObjectMeta.Name,
167 Namespace: s.Namespace,
168 }, d)
169 return errors.IsNotFound(err)
170 }, s.timeout, s.tick, "DNSRecordSet was never garbage collected")
171 }
172
173 func (s *Suite) TestDNSRecordSSA() {
174 addy := s.createAddy("server-side-apply", true)
175
176 s.NoError(s.Client.Create(s.ctx, addy))
177
178 d := &dns.DNSRecordSet{}
179 s.Eventually(func() bool {
180 err := s.Client.Get(s.ctx, types.NamespacedName{
181 Name: addy.ObjectMeta.Name,
182 Namespace: s.Namespace,
183 }, d)
184 return !errors.IsNotFound(err)
185 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
186 s.Equal(s.domain, d.Spec.Name)
187
188 s.Eventually(func() bool {
189 err := s.Client.Get(s.ctx, types.NamespacedName{
190 Name: addy.ObjectMeta.Name,
191 Namespace: addy.ObjectMeta.Namespace,
192 }, addy)
193 return !errors.IsNotFound(err)
194 }, s.timeout, s.tick, "could not retrieve latest computeaddress")
195
196 testChangeDomain := "test.change.domain"
197 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, NameAnnotation, testChangeDomain)
198 s.NoError(s.Client.Update(s.ctx, addy))
199
200 s.Eventually(func() bool {
201 err := s.Client.Get(s.ctx, types.NamespacedName{
202 Name: addy.ObjectMeta.Name,
203 Namespace: s.Namespace,
204 }, d)
205 return !errors.IsNotFound(err) && d.ObjectMeta.Generation != 1
206 }, s.timeout, s.tick, "expected DNSRecordSet was never updated")
207 s.Equal(testChangeDomain, d.Spec.Name)
208 }
209
210 func (s *Suite) TestDNSRecordMigrateToNewProject() {
211 addy := s.createAddy("migrate-projects", true)
212 s.Require().NoError(s.Client.Create(s.ctx, addy))
213 d := &dns.DNSRecordSet{}
214 s.Eventually(func() bool {
215 err := s.Client.Get(s.ctx, types.NamespacedName{
216 Name: addy.ObjectMeta.Name,
217 Namespace: s.Namespace,
218 }, d)
219 return !errors.IsNotFound(err)
220 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
221 s.Equal(s.domain, d.Spec.Name)
222
223 s.Eventually(func() bool {
224 err := s.Client.Get(s.ctx, types.NamespacedName{
225 Name: addy.ObjectMeta.Name,
226 Namespace: addy.ObjectMeta.Namespace,
227 }, addy)
228 return !errors.IsNotFound(err)
229 }, s.timeout, s.tick, "could not retrieve latest computeaddress")
230
231 testDuplicateToNewProject := "ret-edge-prod-dennys"
232 recordConfigs := []RecordConfig{
233 {
234 Name: s.domain,
235 ManagedZone: s.zone,
236 DNSProjectID: testDuplicateToNewProject,
237 },
238 }
239 recordConfigAnnoVal, _ := json.Marshal(recordConfigs)
240 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, RecordConfigsAnnotation, string(recordConfigAnnoVal))
241 s.Require().NoError(s.Client.Update(s.ctx, addy))
242
243 outName := addy.ObjectMeta.Name + genSuffix(0)
244 s.Eventually(func() bool {
245 err := s.Client.Get(s.ctx, types.NamespacedName{
246 Name: outName,
247 Namespace: s.Namespace,
248 }, d)
249 return !errors.IsNotFound(err)
250 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
251 s.Equal(testDuplicateToNewProject, d.ObjectMeta.Annotations[meta.ProjectAnnotation])
252 }
253
254 func (s *Suite) TestDNSRecordFromRecordConfigs() {
255 addy := s.createAddy("from-record-configs", false)
256 id := "uniq."
257
258 recordConfigs := []RecordConfig{
259 {
260 Name: s.domain,
261 ManagedZone: s.zone,
262 DNSProjectID: s.dnsProjectID,
263 },
264 {
265 Name: id + s.domain,
266 ManagedZone: s.zone,
267 DNSProjectID: s.dnsProjectID,
268 },
269 {
270 Name: s.domain,
271 ManagedZone: id + s.zone,
272 DNSProjectID: s.dnsProjectID,
273 },
274 {
275 Name: s.domain,
276 ManagedZone: s.zone,
277 DNSProjectID: id + s.dnsProjectID,
278 },
279 }
280 recordConfigAnnoVal, err := json.Marshal(recordConfigs)
281 s.Require().NoError(err)
282 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, RecordConfigsAnnotation, string(recordConfigAnnoVal))
283 s.Require().NoError(s.Client.Create(s.ctx, addy))
284
285 for i := 0; i < 4; i++ {
286 drsName := fmt.Sprintf("%s%s", addy.ObjectMeta.Name, genSuffix(i))
287 d0 := &dns.DNSRecordSet{}
288 s.Require().Eventually(func() bool {
289 err := s.Client.Get(s.ctx, types.NamespacedName{
290 Name: drsName,
291 Namespace: s.Namespace,
292 }, d0)
293 return !errors.IsNotFound(err)
294 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
295 }
296 }
297
298 func (s *Suite) TestDNSRecordPreventDuplicates() {
299 addy := s.createAddy("prevent-duplicates", true)
300 recordConfigs := []RecordConfig{
301 {
302 Name: s.domain,
303 ManagedZone: s.zone,
304 DNSProjectID: s.dnsProjectID,
305 },
306 }
307 recordConfigAnnoVal, err := json.Marshal(recordConfigs)
308 s.NoError(err)
309 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, RecordConfigsAnnotation, string(recordConfigAnnoVal))
310 s.NoError(s.Client.Create(s.ctx, addy))
311
312
313 outName0 := addy.ObjectMeta.Name + genSuffix(0)
314 d0 := &dns.DNSRecordSet{}
315 s.Never(func() bool {
316 err := s.Client.Get(s.ctx, types.NamespacedName{
317 Name: outName0,
318 Namespace: s.Namespace,
319 }, d0)
320 return !errors.IsNotFound(err)
321 }, s.timeout, s.tick, "DNSRecordSet created when it shouldn't have been")
322
323
324 d := &dns.DNSRecordSet{}
325 s.Never(func() bool {
326 err := s.Client.Get(s.ctx, types.NamespacedName{
327 Name: addy.ObjectMeta.Name,
328 Namespace: s.Namespace,
329 }, d)
330 return !errors.IsNotFound(err)
331 }, s.timeout, s.tick, "DNSRecordSet created when it shouldn't have been")
332 }
333
334 func (s *Suite) TestDNSRecordConfigFromYaml() {
335 computeAddressYaml := []byte(fmt.Sprintf(`
336 apiVersion: compute.cnrm.cloud.google.com/v1beta1
337 kind: ComputeAddress
338 metadata:
339 name: edge-ingress-ip
340 namespace: %s
341 annotations:
342 dns.edge.ncr.com/record-configs: |
343 [
344 {
345 "name":"dev0.edge-preprod.dev.",
346 "managed-zone":"infra/edge-preprod-dns-zone",
347 "dns-project-id":"ret-edge-pltf-infra"
348 },
349 {
350 "name":"dev0.edge-preprod.dev.",
351 "managed-zone":"preprod-infra/edge-preprod-dns-managed-zone",
352 "dns-project-id":"ret-edge-pltf-preprod-infra"
353 }
354 ]
355 spec:
356 location: global
357 resourceID: edge-ingress-ip
358 `, s.Namespace))
359
360 addy := &compute.ComputeAddress{}
361 s.Require().NoError(
362 yaml.Unmarshal(computeAddressYaml, addy),
363 "failed to unmarshal computeaddress from json document string",
364 )
365 ip := "192.158.1.20"
366 addy.Spec = compute.ComputeAddressSpec{Address: &ip}
367 addy.Status = compute.ComputeAddressStatus{
368 Conditions: []v1alpha1.Condition{
369 {Type: "Ready", Status: v1.ConditionTrue, Reason: "I said so"},
370 },
371 }
372 s.Require().NoError(s.Client.Create(s.ctx, addy))
373
374
375 outName0 := addy.ObjectMeta.Name + genSuffix(0)
376 outNamespace0 := addy.ObjectMeta.Namespace
377 d0 := &dns.DNSRecordSet{}
378 s.Require().Eventually(func() bool {
379 err := s.Client.Get(s.ctx, types.NamespacedName{
380 Name: outName0,
381 Namespace: outNamespace0,
382 }, d0)
383 return !errors.IsNotFound(err)
384 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
385 }
386
387 func (s *Suite) TestDeletionPolicyAnnoIsCopied() {
388 tname := "deletion-policy-is-copied"
389 dname := fmt.Sprintf("%s.%s", tname, s.domain)
390 addy := s.createAddy(tname, false)
391 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, meta.DeletionPolicyAnnotation, meta.DeletionPolicyAbandon)
392 recordConfigs := []RecordConfig{
393 {
394 Name: dname,
395 ManagedZone: s.zone,
396 DNSProjectID: s.dnsProjectID,
397 },
398 }
399 recordConfigAnnoVal, err := json.Marshal(recordConfigs)
400 s.NoError(err)
401 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, RecordConfigsAnnotation, string(recordConfigAnnoVal))
402 s.NoError(s.Client.Create(s.ctx, addy))
403
404
405 d := s.eventuallyGetRecordSet(addy.ObjectMeta.Name+genSuffix(0), s.Namespace)
406 s.Require().Equal(meta.DeletionPolicyAbandon, d.Annotations[meta.DeletionPolicyAnnotation])
407 }
408
409 func (s *Suite) TestDeletionPolicyAnnoEmpty() {
410 tname := "deletion-policy-is-empty"
411 dname := fmt.Sprintf("%s.%s", tname, s.domain)
412 addy := s.createAddy(tname, false)
413 recordConfigs := []RecordConfig{
414 {
415 Name: dname,
416 ManagedZone: s.zone,
417 DNSProjectID: s.dnsProjectID,
418 },
419 }
420 recordConfigAnnoVal, err := json.Marshal(recordConfigs)
421 s.NoError(err)
422 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, RecordConfigsAnnotation, string(recordConfigAnnoVal))
423 s.NoError(s.Client.Create(s.ctx, addy))
424
425
426 d := s.eventuallyGetRecordSet(addy.ObjectMeta.Name+genSuffix(0), s.Namespace)
427 s.Require().Equal("", d.Annotations[meta.DeletionPolicyAnnotation])
428 }
429
430 func (s *Suite) eventuallyGetRecordSet(name, namespace string) *dns.DNSRecordSet {
431 d := &dns.DNSRecordSet{}
432 s.Require().Eventually(func() bool {
433 err := s.Client.Get(s.ctx, types.NamespacedName{
434 Name: name,
435 Namespace: namespace,
436 }, d)
437 return !errors.IsNotFound(err)
438 }, s.timeout, s.tick, "expected DNSRecordSet was never created")
439
440 return d
441 }
442
443 func (s *Suite) createAddy(name string, addAnnos bool) *compute.ComputeAddress {
444 addy := &compute.ComputeAddress{
445 ObjectMeta: metav1.ObjectMeta{
446 Name: name,
447 Namespace: s.Namespace,
448 },
449 Spec: compute.ComputeAddressSpec{Location: "global"},
450 }
451 meta.SetProjectAnnotation(&addy.ObjectMeta, s.projectID)
452 if addAnnos {
453 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, NameAnnotation, s.domain)
454 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, ManagedZoneAnnotation, s.zone)
455 metav1.SetMetaDataAnnotation(&addy.ObjectMeta, DNSProjectAnnotation, s.dnsProjectID)
456 }
457
458 if !integration.IsIntegrationTest() {
459 ip := "192.158.1.20"
460 addy.Spec = compute.ComputeAddressSpec{Address: &ip}
461 addy.Status = compute.ComputeAddressStatus{
462 Conditions: []v1alpha1.Condition{
463 {Type: "Ready", Status: v1.ConditionTrue, Reason: "I said so"},
464 },
465 }
466 }
467
468 return addy
469 }
470
View as plain text