Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ingress): Watch ingresses and create monitor when annotations match
Browse files Browse the repository at this point in the history
gabe565 committed Apr 17, 2024
1 parent 6bd49bd commit 52c8d95
Showing 25 changed files with 471 additions and 20 deletions.
5 changes: 5 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
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
@@ -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
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
@@ -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
26 changes: 26 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
@@ -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:
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ module github.com/clevyr/uptime-robot-operator
go 1.22.2

require (
github.com/knadh/koanf/maps v0.1.1
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
@@ -37,6 +39,8 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -58,6 +58,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -69,6 +71,12 @@ 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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
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=
230 changes: 230 additions & 0 deletions internal/controller/ingress_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
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/clevyr/uptime-robot-operator/internal/util"

Check failure on line 25 in internal/controller/ingress_controller.go

GitHub Actions / Test

no required module provides package github.com/clevyr/uptime-robot-operator/internal/util; to add it:
"github.com/knadh/koanf/maps"
"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/controller-runtime@v0.17.2/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 controllerutil.ContainsFinalizer(ingress, myFinalizerName) {
// Delete existing Monitor
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{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
util.DecodeHookMetav1Duration,
mapstructure.TextUnmarshallerHookFunc(),
),
TagName: "json",
WeaklyTypedInput: true,
Result: &monitor.Spec,
})
if err != nil {
return err
}

expanded := make(map[string]any, len(annotations))
for k, v := range annotations {
expanded[k] = v
}
expanded = maps.Unflatten(expanded, ".")
return dec.Decode(expanded)
}
30 changes: 30 additions & 0 deletions internal/controller/ingress_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
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 (
. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("Ingress Controller", func() {
Context("When reconciling a resource", func() {
It("should successfully reconcile the resource", func() {
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})
10 changes: 10 additions & 0 deletions internal/controller/monitor_controller.go
Original file line number Diff line number Diff line change
@@ -188,6 +188,16 @@ func (r *MonitorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct

// SetupWithManager sets up the controller with the Manager.
func (r *MonitorReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &uptimerobotv1.Monitor{}, "spec.sourceRef", func(rawObj client.Object) []string {
monitor := rawObj.(*uptimerobotv1.Monitor)
if monitor.Spec.SourceRef == nil {
return nil
}
return []string{monitor.Spec.SourceRef.Kind + "/" + monitor.Spec.SourceRef.Name}
}); err != nil {
return err
}

return ctrl.NewControllerManagedBy(mgr).
For(&uptimerobotv1.Monitor{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Complete(r)
5 changes: 5 additions & 0 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
@@ -37,6 +37,8 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

networkingv1 "k8s.io/api/networking/v1"

uptimerobotv1 "github.com/clevyr/uptime-robot-operator/api/v1"
//+kubebuilder:scaffold:imports
)
@@ -83,6 +85,9 @@ var _ = BeforeSuite(func() {
err = uptimerobotv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())

err = networkingv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())

//+kubebuilder:scaffold:scheme

k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
2 changes: 1 addition & 1 deletion internal/uptimerobot/client.go
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ func (c Client) MonitorValues(monitor Monitor, form url.Values, contacts Monitor
if monitor.Keyword != nil {
form.Set("keyword_type", strconv.Itoa(int(monitor.Keyword.Type)))
caseType := "1"
if monitor.Keyword.CaseSensitive {
if *monitor.Keyword.CaseSensitive {
caseType = "0"
}
form.Set("keyword_case_type", caseType)
8 changes: 5 additions & 3 deletions internal/uptimerobot/monitor.go
Original file line number Diff line number Diff line change
@@ -22,15 +22,15 @@ type Monitor struct {

// Interval is the monitoring interval.
//+kubebuilder:default:="60s"
Interval metav1.Duration `json:"interval,omitempty"`
Interval *metav1.Duration `json:"interval,omitempty"`

// Status toggles pause status for the monitor. 0 is paused, 1 is running.
//+kubebuilder:default:=1
Status uint8 `json:"status,omitempty"`

// Timeout is the monitor timeout.
//+kubebuilder:default:="30s"
Timeout metav1.Duration `json:"timeout,omitempty"`
Timeout *metav1.Duration `json:"timeout,omitempty"`

// HTTPMethod defines the HTTP verb to use.
//+kubebuilder:default:="HEAD"
@@ -46,11 +46,13 @@ type Monitor struct {
Auth *MonitorAuth `json:"auth,omitempty"`
}

//+kubebuilder:object:generate=true

type MonitorKeyword struct {
Type urtypes.KeywordType `json:"type"`

//+kubebuilder:default:=false
CaseSensitive bool `json:"caseSensitive,omitempty"`
CaseSensitive *bool `json:"caseSensitive,omitempty"`

Value string `json:"value"`
}
2 changes: 1 addition & 1 deletion internal/uptimerobot/urtypes/httpmethod.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package urtypes

//go:generate enumer -type HTTPMethod -json -trimprefix HTTP
//go:generate enumer -type HTTPMethod -trimprefix HTTP -json -text

//+kubebuilder:validation:Type:=string
//+kubebuilder:validation:Enum:=HEAD;GET;POST;PUT;PATCH;DELETE;OPTIONS
14 changes: 13 additions & 1 deletion internal/uptimerobot/urtypes/httpmethod_enumer.go
2 changes: 1 addition & 1 deletion internal/uptimerobot/urtypes/keywordtype.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package urtypes

//go:generate enumer -type KeywordType -json -trimprefix Keyword
//go:generate enumer -type KeywordType -trimprefix Keyword -json -text

//+kubebuilder:validation:Type:=string
//+kubebuilder:validation:Enum:=Exists;NotExists
14 changes: 13 additions & 1 deletion internal/uptimerobot/urtypes/keywordtype_enumer.go
2 changes: 1 addition & 1 deletion internal/uptimerobot/urtypes/monitorauthtype.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package urtypes

//go:generate enumer -type MonitorAuthType -json -trimprefix Auth
//go:generate enumer -type MonitorAuthType -trimprefix Auth -json -text

//+kubebuilder:validation:Type:=string
//+kubebuilder:validation:Enum:=Basic;Digest
14 changes: 13 additions & 1 deletion internal/uptimerobot/urtypes/monitorauthtype_enumer.go
2 changes: 1 addition & 1 deletion internal/uptimerobot/urtypes/monitortype.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package urtypes

//go:generate enumer -type MonitorType -json -trimprefix Type
//go:generate enumer -type MonitorType -trimprefix Type -json -text

//+kubebuilder:validation:Type:=string
//+kubebuilder:validation:Enum:=HTTPS;Keyword;Ping;Port;Heartbeat
14 changes: 13 additions & 1 deletion internal/uptimerobot/urtypes/monitortype_enumer.go
2 changes: 1 addition & 1 deletion internal/uptimerobot/urtypes/porttype.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package urtypes

//go:generate enumer -type PortType -json -trimprefix Port
//go:generate enumer -type PortType -trimprefix Port -json -text

//+kubebuilder:validation:Type:=string
//+kubebuilder:validation:Enum:=HTTP;FTP;SMTP;POP3;IMAP;Custom
14 changes: 13 additions & 1 deletion internal/uptimerobot/urtypes/porttype_enumer.go
38 changes: 34 additions & 4 deletions internal/uptimerobot/zz_generated.deepcopy.go

0 comments on commit 52c8d95

Please sign in to comment.