Skip to content

Commit

Permalink
feat(ingress): Watch ingresses and create monitor when annotations match
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe565 committed Apr 16, 2024
1 parent 6bd49bd commit eb0ccf3
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 10 deletions.
5 changes: 5 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ resources:
group: uptime-robot
kind: Account
path: github.com/clevyr/uptime-robot-operator/api/v1
- controller: true
domain: k8s.io
group: networking
kind: Ingress
path: k8s.io/api/networking/v1
version: v1
version: "3"
5 changes: 4 additions & 1 deletion api/v1/monitor_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type MonitorContactRef struct {
type MonitorSpec struct {
// Interval defines the reconcile interval.
//+kubebuilder:default:="24h"
Interval metav1.Duration `json:"interval,omitempty"`
Interval *metav1.Duration `json:"interval,omitempty"`

// Prune enables garbage collection.
//+kubebuilder:default:=true
Expand All @@ -48,6 +48,9 @@ type MonitorSpec struct {

// +kubebuilder:default:={{}}
Contacts []MonitorContactRef `json:"contacts,omitempty"`

// SourceRef optionally references the object that created this Monitor.
SourceRef *corev1.TypedLocalObjectReference `json:"sourceRef,omitempty"`
}

// MonitorStatus defines the observed state of Monitor
Expand Down
13 changes: 12 additions & 1 deletion api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ func main() {
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Account")
}
if err = (&controller.IngressReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Ingress")
os.Exit(1)
}
//+kubebuilder:scaffold:builder
Expand Down
21 changes: 21 additions & 0 deletions config/crd/bases/uptime-robot.clevyr.com_monitors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,27 @@ spec:
default: true
description: Prune enables garbage collection.
type: boolean
sourceRef:
description: SourceRef optionally references the object that created
this Monitor.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
required:
- monitor
type: object
Expand Down
26 changes: 26 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ rules:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses/finalizers
verbs:
- update
- apiGroups:
- networking.k8s.io
resources:
- ingresses/status
verbs:
- get
- patch
- update
- apiGroups:
- uptime-robot.clevyr.com
resources:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/clevyr/uptime-robot-operator
go 1.22.2

require (
github.com/mitchellh/mapstructure v1.5.0
github.com/onsi/ginkgo/v2 v2.17.1
github.com/onsi/gomega v1.32.0
k8s.io/api v0.29.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
229 changes: 229 additions & 0 deletions internal/controller/ingress_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"context"
"net/url"
"strings"

uptimerobotv1 "github.com/clevyr/uptime-robot-operator/api/v1"
"github.com/mitchellh/mapstructure"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)

const AnnotationPrefix = "uptime-robot.clevyr.com/"

// IngressReconciler reconciles a Ingress object
type IngressReconciler struct {
client.Client
Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *IngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)

ingress := &networkingv1.Ingress{}
if err := r.Get(ctx, req.NamespacedName, ingress); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

list, err := r.findMonitors(ctx, ingress)
if err != nil {
return ctrl.Result{}, err
}

const myFinalizerName = "uptime-robot.clevyr.com/finalizer"
if !ingress.DeletionTimestamp.IsZero() {
// Object is being deleted
if controllerutil.ContainsFinalizer(ingress, myFinalizerName) {
for _, monitor := range list.Items {
if err := r.Delete(ctx, &monitor); err != nil {
return ctrl.Result{}, err
}
}

controllerutil.RemoveFinalizer(ingress, myFinalizerName)
if err := r.Update(ctx, ingress); err != nil {
return ctrl.Result{}, err
}
}

return ctrl.Result{}, nil
}

annotationCount := r.countMatchingAnnotations(ingress)
var create bool
if annotationCount == 0 {
if len(list.Items) != 0 {
// Delete existing Monitor
for _, monitor := range list.Items {
if err := r.Delete(ctx, &monitor); err != nil {
return ctrl.Result{}, err
}
}

if controllerutil.ContainsFinalizer(ingress, myFinalizerName) {
for _, monitor := range list.Items {
if err := r.Delete(ctx, &monitor); err != nil {
return ctrl.Result{}, err
}
}

controllerutil.RemoveFinalizer(ingress, myFinalizerName)
if err := r.Update(ctx, ingress); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
} else {
if len(list.Items) == 0 {
// Create new Monitor
create = true
list.Items = append(list.Items, uptimerobotv1.Monitor{
ObjectMeta: metav1.ObjectMeta{
Name: ingress.Name,
Namespace: req.Namespace,
},
Spec: uptimerobotv1.MonitorSpec{
SourceRef: &corev1.TypedLocalObjectReference{
Kind: ingress.Kind,
Name: ingress.Name,
},
},
})
}
}

annotations := r.getMatchingAnnotations(ingress)
for _, monitor := range list.Items {
if err := r.updateValues(ingress, &monitor, annotations); err != nil {
return ctrl.Result{}, err
}

if create {
if err := r.Create(ctx, &monitor); err != nil {
return ctrl.Result{}, err
}
} else {
if err := r.Update(ctx, &monitor); err != nil {
return ctrl.Result{}, err
}
}
}

if !controllerutil.ContainsFinalizer(ingress, myFinalizerName) {
controllerutil.AddFinalizer(ingress, myFinalizerName)
if err := r.Update(ctx, ingress); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&networkingv1.Ingress{}).
Complete(r)
}

func (r *IngressReconciler) findMonitors(ctx context.Context, ingress *networkingv1.Ingress) (*uptimerobotv1.MonitorList, error) {
list := &uptimerobotv1.MonitorList{}
err := r.Client.List(ctx, list, &client.ListOptions{
FieldSelector: fields.OneTermEqualSelector("spec.sourceRef", ingress.Kind+"/"+ingress.Name),
})
if err != nil {
return list, err
}
return list, nil
}

func (r *IngressReconciler) countMatchingAnnotations(ingress *networkingv1.Ingress) uint {
var count uint
for k := range ingress.Annotations {
if strings.HasPrefix(k, AnnotationPrefix) {
count++
}
}
return count
}

func (r *IngressReconciler) getMatchingAnnotations(ingress *networkingv1.Ingress) map[string]string {
annotations := make(map[string]string, r.countMatchingAnnotations(ingress))
for k, v := range ingress.Annotations {
if strings.HasPrefix(k, AnnotationPrefix) {
annotations[strings.TrimPrefix(k, AnnotationPrefix)] = v
}
}
return annotations
}

func (r *IngressReconciler) updateValues(ingress *networkingv1.Ingress, monitor *uptimerobotv1.Monitor, annotations map[string]string) error {
monitor.Spec.Monitor.FriendlyName = ingress.Name
if _, ok := annotations["monitor.url"]; !ok {
if len(ingress.Spec.Rules) != 0 {
var u url.URL
if u.Scheme, ok = annotations["monitor.scheme"]; !ok {
if len(ingress.Spec.TLS) == 0 {
u.Scheme = "http"
} else {
u.Scheme = "https"
}
}
rule := ingress.Spec.Rules[0]
if u.Host, ok = annotations["monitor.host"]; !ok {
u.Host = rule.Host
}
if u.Path, ok = annotations["monitor.path"]; !ok && len(rule.HTTP.Paths) != 0 {
if path := rule.HTTP.Paths[0].Path; path != "/" {
u.Path = path
}
}
monitor.Spec.Monitor.URL = u.String()
}
}

dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: &monitor.Spec,
})
if err != nil {
return err
}

return dec.Decode(annotations)
}
Loading

0 comments on commit eb0ccf3

Please sign in to comment.