Skip to content

Commit d29b901

Browse files
committed
feat(ingress): iterate over hosts and paths
* Add ability to create API Checks based on ingress resource host and path entries * Determine how internal resources are linked together * Add logic to add / update / delete internal ApiCheck resources * Small code improvements
1 parent 5366d89 commit d29b901

1 file changed

Lines changed: 218 additions & 68 deletions

File tree

Lines changed: 218 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2022.
2+
Copyright 2024.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package networking
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223

2324
checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1"
2425
networkingv1 "k8s.io/api/networking/v1"
@@ -27,6 +28,7 @@ import (
2728
"k8s.io/apimachinery/pkg/runtime"
2829
ctrl "sigs.k8s.io/controller-runtime"
2930
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3032
"sigs.k8s.io/controller-runtime/pkg/log"
3133
)
3234

@@ -51,81 +53,141 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
5153
logger := log.FromContext(ctx)
5254
logger.Info("Reconciler started")
5355

54-
ingress := &networkingv1.Ingress{}
55-
apiCheck := &checklyv1alpha1.ApiCheck{}
56-
56+
// ////////////////////////////////
57+
// Setup
58+
// ///////////////////////////////
59+
ingress := networkingv1.Ingress{}
60+
// apiCheck := &checklyv1alpha1.ApiCheck{}
61+
annotationHost := "k8s.checklyhq.com"
62+
annotationEnabled := fmt.Sprintf("%s/enabled", annotationHost)
63+
checklyFinalizer := "k8s.checklyhq.com/finalizer"
5764
// Check if ingress object is still present
58-
err := r.Get(ctx, req.NamespacedName, ingress)
65+
err := r.Get(ctx, req.NamespacedName, &ingress)
5966
if err != nil {
6067
if errors.IsNotFound(err) {
6168
logger.Info("Ingress got deleted")
62-
return ctrl.Result{}, nil
69+
return ctrl.Result{}, client.IgnoreNotFound(err)
6370
}
6471
logger.Error(err, "Can't read the Ingress object")
6572
return ctrl.Result{}, err
6673
}
6774
logger.Info("Ingress Object found")
6875

69-
// Check if annotation is present on the object
70-
checklyAnnotation := ingress.Annotations["k8s.checklyhq.com/enabled"] == "true"
71-
if !checklyAnnotation {
72-
// Annotation may have been removed or updated, we have to determine if we need to delete a previously created ApiCheck resource
73-
logger.Info("annotation is not present, checking if ApiCheck was created")
74-
err = r.Get(ctx, req.NamespacedName, apiCheck)
75-
if err != nil {
76-
logger.Info("Apicheck not present")
77-
return ctrl.Result{}, nil
78-
}
79-
logger.Info("ApiCheck is present, but we need to delete it")
80-
err = r.Delete(ctx, apiCheck)
81-
if err != nil {
82-
logger.Info("Failed to delete ApiCheck")
83-
return ctrl.Result{}, err
84-
// }
85-
}
86-
76+
// Do we want to do anything with the ingress?
77+
// TODO: What if the annotation was deleted?
78+
_, checklyAnnotationExists := ingress.Annotations[annotationEnabled]
79+
if !checklyAnnotationExists {
80+
logger.Info("Ingress does not have checkly operator annotations, skipping.", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
8781
return ctrl.Result{}, nil
8882
}
8983

9084
// Gather data for the checkly check
91-
apiCheckSpec, err := r.gatherApiCheckData(ingress)
85+
logger.Info("Gathering data for the check")
86+
apiCheckResources, err := r.gatherApiCheckData(&ingress)
9287
if err != nil {
93-
logger.Info("unable to gather data for the apiCheck resource")
88+
logger.Info("unable to gather data for the apiCheck resource", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
9489
return ctrl.Result{}, err
9590
}
91+
// ////////////////////////////////
92+
// Delete Logic
93+
// ///////////////////////////////
94+
95+
if ingress.GetDeletionTimestamp() != nil {
96+
if controllerutil.ContainsFinalizer(&ingress, checklyFinalizer) {
97+
logger.Info("Finalizer present, need to delete ApiCheck first.", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
98+
99+
for _, apiCheckResource := range apiCheckResources {
100+
101+
logger.Info("Checking if ApiCheck was created", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
102+
err = r.Get(ctx, req.NamespacedName, apiCheckResource)
103+
if err != nil {
104+
logger.Info("ApiCheck resource is not present, we don't need to do anything.", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
105+
continue
106+
}
96107

97-
// Check and see if the ApiCheck has been created before
98-
err = r.Get(ctx, req.NamespacedName, apiCheck)
99-
if err == nil {
100-
logger.Info("apiCheck exists, doing an update")
101-
// We can reference the exiting apiCheck object that the server returned
102-
apiCheck.Spec = apiCheckSpec
103-
err = r.Update(ctx, apiCheck)
108+
logger.Info("ApiCheck resource is present, we need to delete it..", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
109+
err = r.Delete(ctx, apiCheckResource)
110+
if err != nil {
111+
logger.Error(err, "Failed to delete ApiCheck", "Name:", apiCheckResource.Name, "Namespace:", apiCheckResource.Namespace)
112+
continue
113+
}
114+
115+
logger.Info("ApiCheck resource deleted successfully.", apiCheckResource.Name, "Namespace:", apiCheckResource.Namespace)
116+
}
117+
118+
// Delete finalizer logic
119+
logger.Info("Deleting finalizer", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
120+
controllerutil.RemoveFinalizer(&ingress, checklyFinalizer)
121+
err = r.Update(ctx, &ingress)
122+
if err != nil {
123+
logger.Error(err, "Failed to delete finalizer", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
124+
return ctrl.Result{}, err
125+
}
126+
logger.Info("Successfully deleted finalizer", "Ingress Name", ingress.Name, "Ingress namespace", ingress.Namespace)
127+
return ctrl.Result{}, nil
128+
}
129+
}
130+
131+
// /////////////////////////////
132+
// Finalizer logic
133+
// ////////////////////////////
134+
if !controllerutil.ContainsFinalizer(&ingress, checklyFinalizer) {
135+
controllerutil.AddFinalizer(&ingress, checklyFinalizer)
136+
err = r.Update(ctx, &ingress)
104137
if err != nil {
138+
logger.Error(err, "Failed to update ingress finalizer")
105139
return ctrl.Result{}, err
106140
}
141+
logger.Info("Added finalizer", "ingress", ingress.Name, "Ingress namespace", ingress.Namespace)
107142
return ctrl.Result{}, nil
108143
}
109144

110-
// Create apiCheck
111-
// We need to write the k8s spec resources as it is a new object
112-
newApiCheck := &checklyv1alpha1.ApiCheck{
113-
ObjectMeta: metav1.ObjectMeta{
114-
Name: ingress.Name,
115-
Namespace: ingress.Namespace,
116-
OwnerReferences: []metav1.OwnerReference{
117-
*metav1.NewControllerRef(ingress, networkingv1.SchemeGroupVersion.WithKind("ingress")),
118-
},
119-
},
120-
Spec: apiCheckSpec,
121-
}
145+
// /////////////////////////////
146+
// Update/Create logic
147+
// ////////////////////////////
122148

123-
err = r.Create(ctx, newApiCheck)
149+
newApiChecks, deleteApiChecks, updateApiChecks, err := r.compareApiChecks(ctx, &ingress, apiCheckResources)
124150
if err != nil {
125-
logger.Info("Failed to create ApiCheck", "err", err)
151+
logger.Error(err, "Failed to list existing API checks")
126152
return ctrl.Result{}, err
127153
}
128154

155+
// Create new Api Checks
156+
if len(newApiChecks) > 0 {
157+
for _, apiCheck := range newApiChecks {
158+
logger.Info("Creating ApiCheck", "ApiCheck Name:", apiCheck.Name)
159+
err = r.Create(ctx, apiCheck)
160+
if err != nil {
161+
logger.Error(err, "Failed to create ApiCheck", "APICheck name:", apiCheck.Name, "Namespace:", apiCheck.Namespace, "Ingress name:", ingress.Name, "Ingress namespace", ingress.Namespace)
162+
return ctrl.Result{}, err
163+
}
164+
}
165+
}
166+
167+
// Update API checks
168+
if len(updateApiChecks) > 0 {
169+
for _, apiCheck := range updateApiChecks {
170+
logger.Info("Updating ApiCheck", "ApiCheck Name:", apiCheck.Name)
171+
err = r.Update(ctx, apiCheck)
172+
if err != nil {
173+
logger.Error(err, "Failed to update APICheck resource", "APICheck name:", apiCheck.Name, "Namespace:", apiCheck.Namespace, "Ingress name:", ingress.Name, "Ingress namespace", ingress.Namespace)
174+
return ctrl.Result{}, err
175+
}
176+
}
177+
}
178+
179+
// Delete old API checks
180+
if len(deleteApiChecks) > 0 {
181+
for _, apiCheck := range deleteApiChecks {
182+
183+
logger.Info("Delete ApiCheck", "ApiCheck Name:", apiCheck.Name)
184+
err = r.Delete(ctx, apiCheck)
185+
if err != nil {
186+
logger.Error(err, "Failed to delete ApiCheck resource", "APICheck name:", apiCheck.Name, "Namespace:", apiCheck.Namespace, "Ingress name:", ingress.Name, "Ingress namespace", ingress.Namespace)
187+
}
188+
}
189+
}
190+
129191
return ctrl.Result{}, nil
130192
}
131193

@@ -136,7 +198,7 @@ func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error {
136198
Complete(r)
137199
}
138200

139-
func (r *IngressReconciler) gatherApiCheckData(ingress *networkingv1.Ingress) (apiCheckSpec checklyv1alpha1.ApiCheckSpec, err error) {
201+
func (r *IngressReconciler) gatherApiCheckData(ingress *networkingv1.Ingress) (apiChecks []*checklyv1alpha1.ApiCheck, err error) {
140202

141203
annotationHost := "k8s.checklyhq.com"
142204
annotationPath := fmt.Sprintf("%s/path", annotationHost)
@@ -145,21 +207,6 @@ func (r *IngressReconciler) gatherApiCheckData(ingress *networkingv1.Ingress) (a
145207
annotationGroup := fmt.Sprintf("%s/group", annotationHost)
146208
annotationMuted := fmt.Sprintf("%s/muted", annotationHost)
147209

148-
// Construct the endpoint
149-
path := ""
150-
if ingress.Annotations[annotationPath] != "" {
151-
path = ingress.Annotations[annotationPath]
152-
}
153-
154-
var host string
155-
if ingress.Annotations[annotationEndpoint] == "" {
156-
host = ingress.Spec.Rules[0].Host
157-
} else {
158-
host = ingress.Annotations[annotationEndpoint]
159-
}
160-
161-
endpoint := fmt.Sprintf("https://%s%s", host, path)
162-
163210
// Expected success code
164211
var success string
165212
if ingress.Annotations[annotationSuccess] != "" {
@@ -184,13 +231,116 @@ func (r *IngressReconciler) gatherApiCheckData(ingress *networkingv1.Ingress) (a
184231
muted = true
185232
}
186233

187-
apiCheckSpec = checklyv1alpha1.ApiCheckSpec{
188-
Endpoint: endpoint,
189-
Group: group,
190-
Success: success,
191-
Muted: muted,
234+
labels := make(map[string]string)
235+
labels["ingress-controller"] = ingress.Name
236+
237+
// Get the host(s) and path(s) from the ingress object
238+
for _, rule := range ingress.Spec.Rules {
239+
240+
// Get the host
241+
var host string
242+
if ingress.Annotations[annotationEndpoint] == "" {
243+
host = rule.Host
244+
} else {
245+
host = ingress.Annotations[annotationEndpoint]
246+
}
247+
248+
// Get the path(s)
249+
// var paths []string
250+
var path string
251+
for _, rulePath := range rule.HTTP.Paths {
252+
if ingress.Annotations[annotationPath] == "" {
253+
if rulePath.Path == "" {
254+
path = "/"
255+
} else {
256+
path = rulePath.Path
257+
}
258+
} else {
259+
path = ingress.Annotations[annotationPath]
260+
}
261+
262+
// Set apiCheck Name
263+
checkName := fmt.Sprintf("%s-%s-%s", ingress.Name, host, path)
264+
checkName = strings.Replace(checkName, "/", "", -1)
265+
checkName = strings.Replace(checkName, ".", "", -1)
266+
checkName = strings.Trim(checkName, "-")
267+
268+
// Set endpoint
269+
endpoint := fmt.Sprintf("%s%s", host, path)
270+
271+
apiCheckSpec := checklyv1alpha1.ApiCheckSpec{
272+
Endpoint: endpoint,
273+
Group: group,
274+
Success: success,
275+
Muted: muted,
276+
}
277+
278+
newApiCheck := &checklyv1alpha1.ApiCheck{
279+
ObjectMeta: metav1.ObjectMeta{
280+
Name: checkName,
281+
Namespace: ingress.Namespace,
282+
OwnerReferences: []metav1.OwnerReference{
283+
*metav1.NewControllerRef(ingress, networkingv1.SchemeGroupVersion.WithKind("ingress")),
284+
},
285+
Labels: labels,
286+
},
287+
Spec: apiCheckSpec,
288+
}
289+
290+
apiChecks = append(apiChecks, newApiCheck)
291+
}
292+
192293
}
193294

194295
// Last return
195296
return
196297
}
298+
299+
func (r *IngressReconciler) compareApiChecks(ctx context.Context, ingress *networkingv1.Ingress, ingressApiChecks []*checklyv1alpha1.ApiCheck) (newApiChecks []*checklyv1alpha1.ApiCheck, deleteApiChecks []*checklyv1alpha1.ApiCheck, updateApiChecks []*checklyv1alpha1.ApiCheck, err error) {
300+
301+
logger := log.FromContext(ctx)
302+
303+
var existingApiChecks checklyv1alpha1.ApiCheckList
304+
labels := make(map[string]*string)
305+
labels["ingress-controller"] = &ingress.Name
306+
err = r.List(ctx, &existingApiChecks, client.InNamespace(ingress.Namespace), client.MatchingLabels{"ingress-controller": ingress.Name})
307+
if err != nil {
308+
return
309+
}
310+
311+
existingApiChecksMap := make(map[string]checklyv1alpha1.ApiCheck)
312+
for _, existingApiCheck := range existingApiChecks.Items {
313+
existingApiChecksMap[existingApiCheck.Name] = existingApiCheck
314+
}
315+
316+
newApiChecksMap := make(map[string]*checklyv1alpha1.ApiCheck)
317+
for _, ingressApiCheck := range ingressApiChecks {
318+
newApiChecksMap[ingressApiCheck.Name] = ingressApiCheck
319+
}
320+
321+
// Compare items
322+
for _, existingApiCheck := range existingApiChecksMap {
323+
_, exists := newApiChecksMap[existingApiCheck.Name]
324+
if exists {
325+
if existingApiCheck.Spec == newApiChecksMap[existingApiCheck.Name].Spec {
326+
logger.Info("ApiCheck data is identical, no need for update", "ApiCheck Name", existingApiCheck.Name)
327+
} else {
328+
logger.Info("ApiCheck data is not identical, update needed", "ApiCheck Name", existingApiCheck.Name, "old spec", existingApiCheck.Spec, "new spec", newApiChecksMap[existingApiCheck.Name].Spec)
329+
updateApiChecks = append(updateApiChecks, newApiChecksMap[existingApiCheck.Name])
330+
}
331+
332+
// Remove items from new api checks map
333+
delete(newApiChecksMap, existingApiCheck.Name)
334+
} else {
335+
logger.Info("ApiCheck is not needed anymore, delete.", "ApiCheck Name", existingApiCheck.Name)
336+
deleteApiChecks = append(deleteApiChecks, &existingApiCheck)
337+
}
338+
}
339+
340+
// Loop over remaining items and add them to the new checks list, these will be created
341+
for _, newApiCheck := range newApiChecksMap {
342+
newApiChecks = append(newApiChecks, newApiCheck)
343+
}
344+
345+
return
346+
}

0 commit comments

Comments
 (0)