11/*
2- Copyright 2022 .
2+ Copyright 2024 .
33
44Licensed under the Apache License, Version 2.0 (the "License");
55you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package networking
1919import (
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