Repository: borchero/switchboard (go)

Issue:
Possible to ignore only specific integration?
Hi!

I know it is possible to annotate IngressRoute resource to make switchboard ignore it:
```
metadata:
  annoations:
    switchboard.borchero.com/ignore: "true"
```
But is it possible to only ignore a specific integration? Or only select specific integration(s) for a resource?

Example use case

We have several sites sharing the same main domain, for example:
- www.example.com
- api.example.com
- xyz.example.com

And we are using cert-manager managed wildcard certificate *.example.com. So the certificate resource is managed separately, and when creating an IngressRoute we would specify the appropriate secret, for example, `star-example-com-tls`. Because of this, we would not want switchboard to attempt to manage this and create a new certificate resource using `star-example-com-tls` secret, and if it did, cert-manager would probably fail operating on the resultant certificate resource, because the target `secretName` resource would already exist, tied to a different certificate resource.

At the same time, we would still like the external-dns resources to be created dynamically.

Of course, the whole cert-manager integration can be disabled, but there still might be other IngressRoute resources which are not covered by the wildcard certificate, and for which switchboard could manage the certificate resources. I would imagine that more similar needs would arise if in the future switchboard would add more possible integrations.

Pull request description:
Allow to ignore only specific integrations
<!-- Please provide an expressive pull request title as it will be listed in the release notes. -->

## Motivation

This PR resolves #20.

## Explanation

This PR introduces the following change to the `switchboard.borchero.com/ignore` annotation:

- A value of `true` OR `all` now ignores all integrations
- Specifying the names of specific annotations allows to ignore particular integrations
  - A list of names is possible (e.g. `cert-manager,external-dns`)

## Changes

- Add a new method `MatchesIntegration` to `switchboard.Selector`
- Call this method in the reconciliation loop of `controllers.IngressRouteReconciler`

## Checklist

- [x] I added relevant tests
- [x] I have updated the documentation if necessary


Patch:
diff --git a/README.md b/README.md
index 79966b7..678062b 100644
--- a/README.md
+++ b/README.md
@@ -163,10 +163,14 @@ also disable processing for individual ingress routes by setting an additional a
 
 ```yaml
 metadata:
-  annoations:
-    switchboard.borchero.com/ignore: "true"
+  annotations:
+    switchboard.borchero.com/ignore: "all"
 ```
 
+By setting the `ignore` annotation to `all` (or `true`), Switchboard does not process the ingress
+route at all. For more fine-grained control, the value of this annotation can also be set to a
+comma-separated list of integrations (possible values `cert-manager`, `external-dns`).
+
 ## License
 
 Switchboard is licensed under the [MIT License](./LICENSE).
diff --git a/internal/controllers/ingressroute.go b/internal/controllers/ingressroute.go
index 93187e9..bd467a0 100644
--- a/internal/controllers/ingressroute.go
+++ b/internal/controllers/ingressroute.go
@@ -80,6 +80,11 @@ func (r *IngressRouteReconciler) Reconcile(
 
 	// Then, we can run the integrations
 	for _, itg := range r.integrations {
+		if !r.selector.MatchesIntegration(ingressRoute.Annotations, itg.Name()) {
+			// If integration is ignored, skip it
+			logger.Debug("ignoring integration", zap.String("integration", itg.Name()))
+			continue
+		}
 		if err := itg.UpdateResource(ctx, &ingressRoute, info); err != nil {
 			logger.Error("failed to upsert resource",
 				zap.String("integration", itg.Name()), zap.Error(err),
diff --git a/internal/switchboard/selector.go b/internal/switchboard/selector.go
index 485f127..c1a81e9 100644
--- a/internal/switchboard/selector.go
+++ b/internal/switchboard/selector.go
@@ -1,5 +1,7 @@
 package switchboard
 
+import "strings"
+
 const (
 	ingressAnnotationKey = "kubernetes.io/ingress.class"
 	ignoreAnnotationKey  = "switchboard.borchero.com/ignore"
@@ -21,7 +23,7 @@ func NewSelector(ingressClass *string) Selector {
 func (s Selector) Matches(annotations map[string]string) bool {
 	// If the ignore annotation is set, selector never matches
 	if ignore, ok := annotations[ignoreAnnotationKey]; ok {
-		if ignore == "true" {
+		if ignore == "true" || ignore == "all" {
 			return false
 		}
 	}
@@ -38,3 +40,20 @@ func (s Selector) Matches(annotations map[string]string) bool {
 	// Otherwise, any ingress class is fine
 	return true
 }
+
+// MatchesIntegration returns whether the provided set of annotations match the provided
+// integration.
+func (s Selector) MatchesIntegration(annotations map[string]string, integration string) bool {
+	if ignore, ok := annotations[ignoreAnnotationKey]; ok {
+		if ignore == "true" || ignore == "all" {
+			return false
+		}
+		// Iterate over list of values set for `ignore` annotation
+		for _, ignored := range strings.Split(ignore, ",") {
+			if strings.TrimSpace(ignored) == integration {
+				return false
+			}
+		}
+	}
+	return true
+}


Test patch:
diff --git a/internal/switchboard/selector_test.go b/internal/switchboard/selector_test.go
index ef5369b..1925d65 100644
--- a/internal/switchboard/selector_test.go
+++ b/internal/switchboard/selector_test.go
@@ -19,6 +19,13 @@ func TestMatchesNoIngressClass(t *testing.T) {
 		"kubernetes.io/ingress.class":     "test",
 		"switchboard.borchero.com/ignore": "true",
 	}))
+	assert.False(t, selector.Matches(map[string]string{
+		"switchboard.borchero.com/ignore": "all",
+	}))
+	assert.False(t, selector.Matches(map[string]string{
+		"kubernetes.io/ingress.class":     "test",
+		"switchboard.borchero.com/ignore": "all",
+	}))
 }
 
 func TestMatchesIngressClass(t *testing.T) {
@@ -42,4 +49,58 @@ func TestMatchesIngressClass(t *testing.T) {
 		"kubernetes.io/ingress.class":     "ingress",
 		"switchboard.borchero.com/ignore": "true",
 	}))
+	assert.False(t, selector.Matches(map[string]string{
+		"switchboard.borchero.com/ignore": "all",
+	}))
+	assert.False(t, selector.Matches(map[string]string{
+		"kubernetes.io/ingress.class":     "test",
+		"switchboard.borchero.com/ignore": "all",
+	}))
+	assert.False(t, selector.Matches(map[string]string{
+		"kubernetes.io/ingress.class":     "ingress",
+		"switchboard.borchero.com/ignore": "all",
+	}))
+}
+
+func TestMatchesIntegration(t *testing.T) {
+	cls := "ingress"
+	selector := NewSelector(&cls)
+
+	// Ignore all
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "true",
+	}, "external-dns"))
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "all",
+	}, "external-dns"))
+
+	// Ignore only one
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns",
+	}, "external-dns"))
+	assert.True(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "cert-manager",
+	}, "external-dns"))
+
+	// Ignore multiple
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns,cert-manager",
+	}, "external-dns"))
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns,cert-manager",
+	}, "cert-manager"))
+	assert.True(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns,cert-manager",
+	}, "unknown"))
+
+	// Ignore with space in between
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns, cert-manager",
+	}, "external-dns"))
+	assert.False(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns, cert-manager",
+	}, "cert-manager"))
+	assert.True(t, selector.MatchesIntegration(map[string]string{
+		"switchboard.borchero.com/ignore": "external-dns, cert-manager",
+	}, "unknown"))
 }