1 package dennis
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "reflect"
8 "strings"
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/fluxcd/pkg/ssa"
15 "github.com/go-logr/logr"
16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18 "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
19 ctrl "sigs.k8s.io/controller-runtime"
20 "sigs.k8s.io/controller-runtime/pkg/client"
21 "sigs.k8s.io/controller-runtime/pkg/event"
22 "sigs.k8s.io/controller-runtime/pkg/predicate"
23
24 "edge-infra.dev/pkg/edge/constants"
25 "edge-infra.dev/pkg/k8s/konfigkonnector/apis/meta"
26 unstructuredutil "edge-infra.dev/pkg/k8s/unstructured"
27 )
28
29
30
31
32
33
34 var (
35 domain = "dns." + constants.Domain
36
37 NameAnnotation = domain + "/name"
38
39
40
41
42 ManagedZoneAnnotation = domain + "/managed-zone"
43
44
45 DNSProjectAnnotation = domain + "/dns-project-id"
46
47
48 DNSExternalZoneAnnotation = domain + "/external"
49
50
51
52 RecordConfigsAnnotation = domain + "/record-configs"
53
54 ttl = 300
55 )
56
57
58
59
60
61
62
63 type ComputeAddressReconciler struct {
64 client.Client
65 Log logr.Logger
66 ResourceManager *ssa.ResourceManager
67 Name string
68 }
69
70
71
72 type RecordConfig struct {
73 Name string `json:"name"`
74 ManagedZone string `json:"managed-zone"`
75 DNSProjectID string `json:"dns-project-id"`
76 External string `json:"external"`
77
78 genSuffix string
79 }
80
81
82
83 func hasRequiredAnnotations(annos map[string]string) bool {
84 _, name := annos[NameAnnotation]
85 _, zone := annos[ManagedZoneAnnotation]
86 _, dns := annos[DNSProjectAnnotation]
87 return name && zone && dns
88 }
89
90 func hasStructuredAnnotation(annos map[string]string) bool {
91 _, ok := annos[RecordConfigsAnnotation]
92 return ok
93 }
94
95 func filter() predicate.Predicate {
96 return predicate.Funcs{
97 UpdateFunc: func(e event.UpdateEvent) bool {
98 a := e.ObjectNew.GetAnnotations()
99 return hasRequiredAnnotations(a) || hasStructuredAnnotation(a)
100 },
101 CreateFunc: func(e event.CreateEvent) bool {
102 a := e.Object.GetAnnotations()
103 return hasRequiredAnnotations(a) || hasStructuredAnnotation(a)
104 },
105 DeleteFunc: func(_ event.DeleteEvent) bool {
106 return false
107 },
108 }
109 }
110
111
112 func (r *ComputeAddressReconciler) SetupWithManager(mgr ctrl.Manager) error {
113 return ctrl.NewControllerManagedBy(mgr).
114 For(&compute.ComputeAddress{}).
115 WithEventFilter(filter()).
116 Complete(r)
117 }
118
119 func (r *ComputeAddressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
120 r.setResourceManager()
121 log := r.Log.WithValues("computeaddress", req.NamespacedName)
122
123 addy := &compute.ComputeAddress{}
124 if err := r.Client.Get(ctx, req.NamespacedName, addy); err != nil {
125 log.Error(err, "failed to get computeaddress")
126 return ctrl.Result{}, err
127 }
128
129 if ready, reason := meta.IsReady(addy.Status.Conditions); !ready {
130 log.Info("waiting for computeaddress to become ready", "reason", reason)
131 return ctrl.Result{Requeue: true, RequeueAfter: 5 * time.Second}, nil
132 }
133
134 cfgs, err := parseRecordConfigsAnnotation(addy.ObjectMeta)
135 if err != nil {
136 log.Error(err, fmt.Sprintf("failed to parse the %s annotation", RecordConfigsAnnotation))
137 return ctrl.Result{}, err
138 }
139
140 if cfg := getRecordConfigFromSingularAnnotations(addy.ObjectMeta); cfg != nil {
141 cfgs = append(cfgs, cfg)
142 }
143
144 if err := validateRecordConfigs(cfgs); err != nil {
145 return ctrl.Result{}, fmt.Errorf("invalid dns configuration: %w", err)
146 }
147
148 uobjs := []*unstructured.Unstructured{}
149 for _, recordCfg := range cfgs {
150 recordName := addy.ObjectMeta.Name + recordCfg.genSuffix
151 dns := buildDNSRecordSet(*recordCfg, recordName, req.Namespace, addy)
152 uobj, err := unstructuredutil.ToUnstructured(dns)
153 if err != nil {
154 return ctrl.Result{}, fmt.Errorf("failed to convert dnsrecordset/%s/%s to unstructured: %w", uobj.GetNamespace(), uobj.GetName(), err)
155 }
156 uobjs = append(uobjs, uobj)
157 }
158 changeset, err := r.ResourceManager.ApplyAll(ctx, uobjs, ssa.ApplyOptions{Force: true})
159 if err != nil {
160 return ctrl.Result{}, fmt.Errorf("failed to apply dnsrecordset(s): %w", err)
161 }
162 log.Info("applied DNSRecordSets", "changeset", changeset.ToMap())
163 return ctrl.Result{}, nil
164 }
165
166 func parseRecordConfigsAnnotation(meta metav1.ObjectMeta) ([]*RecordConfig, error) {
167 cfgs := []*RecordConfig{}
168 if metav1.HasAnnotation(meta, RecordConfigsAnnotation) {
169 if err := json.Unmarshal([]byte(meta.Annotations[RecordConfigsAnnotation]), &cfgs); err != nil {
170 return nil, err
171 }
172 }
173 for i, cfg := range cfgs {
174 cfg.genSuffix = genSuffix(i)
175 }
176 return cfgs, nil
177 }
178
179 func getRecordConfigFromSingularAnnotations(meta metav1.ObjectMeta) *RecordConfig {
180 if hasRequiredAnnotations(meta.Annotations) {
181 return &RecordConfig{
182 Name: meta.Annotations[NameAnnotation],
183 ManagedZone: meta.Annotations[ManagedZoneAnnotation],
184 DNSProjectID: meta.Annotations[DNSProjectAnnotation],
185 External: meta.Annotations[DNSExternalZoneAnnotation],
186 }
187 }
188 return nil
189 }
190
191 func buildDNSRecordSet(cfg RecordConfig, name, namespace string, addy *compute.ComputeAddress) *dns.DNSRecordSet {
192 owner := *metav1.NewControllerRef(addy, compute.ComputeAddressGVK)
193 dns := &dns.DNSRecordSet{
194 TypeMeta: metav1.TypeMeta{
195 Kind: reflect.TypeOf(dns.DNSRecordSet{}).Name(),
196 APIVersion: dns.SchemeGroupVersion.String(),
197 },
198 ObjectMeta: metav1.ObjectMeta{
199 Name: name,
200 Namespace: namespace,
201
202
203 OwnerReferences: []metav1.OwnerReference{owner},
204 Annotations: map[string]string{meta.ProjectAnnotation: cfg.DNSProjectID},
205 },
206 Spec: dns.DNSRecordSetSpec{
207 Name: cfg.Name,
208 Rrdatas: []string{*addy.Spec.Address},
209 Type: "A",
210 Ttl: &ttl,
211 ManagedZoneRef: v1alpha1.ResourceRef{},
212 },
213 }
214
215
216 if policy, ok := addy.ObjectMeta.Annotations[meta.DeletionPolicyAnnotation]; ok {
217 dns.ObjectMeta.Annotations[meta.DeletionPolicyAnnotation] = policy
218 }
219
220 zone := cfg.ManagedZone
221 zoneTokens := strings.Split(zone, "/")
222 external := cfg.External
223 if external == "true" {
224 dns.Spec.ManagedZoneRef.External = zoneTokens[0]
225 } else {
226
227 if len(zoneTokens) == 1 {
228 dns.Spec.ManagedZoneRef.Name = zoneTokens[0]
229 } else {
230 dns.Spec.ManagedZoneRef.Namespace = zoneTokens[0]
231 dns.Spec.ManagedZoneRef.Name = zoneTokens[1]
232 }
233 }
234 return dns
235 }
236
237 func (r *ComputeAddressReconciler) setResourceManager() {
238 if r.ResourceManager == nil {
239 mgr := ssa.NewResourceManager(
240 r.Client,
241 polling.NewStatusPoller(r.Client, r.Client.RESTMapper(), polling.Options{}), ssa.Owner{Field: r.Name},
242 )
243 r.ResourceManager = mgr
244 }
245 }
246
247 func validateRecordConfigs(cfgs []*RecordConfig) error {
248
249 uniqDomainsByProjectZone := map[string]bool{}
250 for _, cfg := range cfgs {
251 key := fmt.Sprintf("%s_%s_%s", cfg.DNSProjectID, cfg.ManagedZone, cfg.Name)
252 if !uniqDomainsByProjectZone[key] {
253 uniqDomainsByProjectZone[key] = true
254 } else {
255 return fmt.Errorf("duplicate domain %s. uniqueness key: %s", cfg.Name, key)
256 }
257
258 errStrFmt := "missing required field %s on object in %s"
259 if cfg.DNSProjectID == "" {
260 return fmt.Errorf(errStrFmt, DNSProjectAnnotation, RecordConfigsAnnotation)
261 }
262 if cfg.ManagedZone == "" {
263 return fmt.Errorf(errStrFmt, ManagedZoneAnnotation, RecordConfigsAnnotation)
264 }
265 if cfg.Name == "" {
266 return fmt.Errorf(errStrFmt, NameAnnotation, RecordConfigsAnnotation)
267 }
268 }
269
270 return nil
271 }
272
273 func genSuffix(num int) string {
274 return fmt.Sprintf("-c%d", num)
275 }
276
View as plain text