Repository: giantswarm/silence-operator
Pull request #534: migrate to kube-builder instead of operatorkit
Author: login: QuentinBisson
Created: 2025-04-28T20:30:01Z | Merged: 2025-05-05T09:08:23Z

## Checklist

- [x] Update changelog in CHANGELOG.md.



Diff:
diff --git a/.abs/main.yaml b/.abs/main.yaml
new file mode 100644
index 00000000..cea9796f
--- /dev/null
+++ b/.abs/main.yaml
@@ -0,0 +1,5 @@
+replace-app-version-with-git: true
+replace-chart-version-with-git: true
+generate-metadata: true
+chart-dir: ./helm/silence-operator
+destination: ./build
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d9796611..f51a114e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -9,6 +9,7 @@ workflows:
     - architect/go-build:
         name: go-build
         binary: silence-operator
+        path: ./cmd
         filters:
           tags:
             only: /^v.*/
@@ -21,14 +22,15 @@ workflows:
         filters:
           tags:
             only: /^v.*/
-
           branches:
             ignore:
             - main
             - master
+
     - architect/push-to-app-catalog:
         context: architect
-        name: push-silence-operator-to-app-catalog
+        executor: app-build-suite
+        name: push-to-app-catalog
         app_catalog: control-plane-catalog
         app_catalog_test: control-plane-test-catalog
         chart: silence-operator
@@ -37,11 +39,11 @@ workflows:
         filters:
           tags:
             only: /^v.*/
-
           branches:
             ignore:
             - main
             - master
+
     - architect/push-to-app-collection:
         name: push-silence-operator-to-aws-app-collection
         context: architect
@@ -49,21 +51,7 @@ workflows:
         app_namespace: monitoring
         app_collection_repo: aws-app-collection
         requires:
-        - push-silence-operator-to-app-catalog
-        filters:
-          branches:
-            ignore: /.*/
-          tags:
-            only: /^v.*/
-
-    - architect/push-to-app-collection:
-        name: push-silence-operator-to-azure-app-collection
-        context: architect
-        app_name: silence-operator
-        app_namespace: monitoring
-        app_collection_repo: azure-app-collection
-        requires:
-        - push-silence-operator-to-app-catalog
+        - push-to-app-catalog
         filters:
           branches:
             ignore: /.*/
@@ -77,7 +65,7 @@ workflows:
         app_namespace: monitoring
         app_collection_repo: vsphere-app-collection
         requires:
-        - push-silence-operator-to-app-catalog
+        - push-to-app-catalog
         filters:
           branches:
             ignore: /.*/
@@ -91,14 +79,13 @@ workflows:
         app_namespace: monitoring
         app_collection_repo: proxmox-app-collection
         requires:
-        - push-silence-operator-to-app-catalog
+        - push-to-app-catalog
         filters:
           branches:
             ignore: /.*/
           tags:
             only: /^v.*/
 
-
     - architect/push-to-app-collection:
         context: architect
         name: push-to-cloud-director-app-collection
@@ -106,7 +93,7 @@ workflows:
         app_namespace: monitoring
         app_collection_repo: cloud-director-app-collection
         requires:
-        - push-silence-operator-to-app-catalog
+        - push-to-app-catalog
         filters:
           branches:
             ignore: /.*/
@@ -120,7 +107,7 @@ workflows:
         app_namespace: monitoring
         app_collection_repo: capa-app-collection
         requires:
-        - push-silence-operator-to-app-catalog
+        - push-to-app-catalog
         filters:
           branches:
             ignore: /.*/
@@ -134,7 +121,7 @@ workflows:
         app_namespace: monitoring
         app_collection_repo: capz-app-collection
         requires:
-        - push-silence-operator-to-app-catalog
+        - push-to-app-catalog
         filters:
           branches:
             ignore: /.*/
diff --git a/.nancy-ignore b/.nancy-ignore
index 8043e0e5..2cfd0114 100644
--- a/.nancy-ignore
+++ b/.nancy-ignore
@@ -1,2 +1,2 @@
-# pkg:golang/google.golang.org/protobuf@v1.32.0
-CVE-2024-24786 until=2024-03-31
+# pkg:golang/k8s.io/apiserver@v0.32.4
+CVE-2020-8561 until=2025-05-31
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 00000000..c04b6160
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,273 @@
+/*
+Copyright 2025.
+
+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 main
+
+import (
+	"crypto/tls"
+	"flag"
+	"os"
+	"path/filepath"
+
+	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
+	// to ensure that exec-entrypoint and run can make use of them.
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/certwatcher"
+	"sigs.k8s.io/controller-runtime/pkg/healthz"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
+	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	monitoringv1alpha1 "github.com/giantswarm/silence-operator/api/v1alpha1"
+	"github.com/giantswarm/silence-operator/internal/controller"
+	"github.com/giantswarm/silence-operator/pkg/alertmanager"
+	// +kubebuilder:scaffold:imports
+)
+
+var (
+	scheme   = runtime.NewScheme()
+	setupLog = ctrl.Log.WithName("setup")
+)
+
+func init() {
+	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+
+	utilruntime.Must(monitoringv1alpha1.AddToScheme(scheme))
+	// +kubebuilder:scaffold:scheme
+}
+
+// nolint:gocyclo
+func main() {
+	var metricsAddr string
+	var metricsCertPath, metricsCertName, metricsCertKey string
+	var webhookCertPath, webhookCertName, webhookCertKey string
+	var enableLeaderElection bool
+	var probeAddr string
+	var secureMetrics bool
+	var enableHTTP2 bool
+	var tlsOpts []func(*tls.Config)
+
+	var alertmanagerAddress, alertmanagerTenantId string
+	var alertmanagerAuthentication bool
+	flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
+		"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
+	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
+	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
+		"Enable leader election for controller manager. "+
+			"Enabling this will ensure there is only one active controller manager.")
+	flag.BoolVar(&secureMetrics, "metrics-secure", false, // TODO See with @shield how to set this up
+		"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
+	flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
+	flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
+	flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
+	flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
+		"The directory that contains the metrics server certificate.")
+	flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
+	flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
+	flag.BoolVar(&enableHTTP2, "enable-http2", false,
+		"If set, HTTP/2 will be enabled for the metrics and webhook servers")
+	flag.StringVar(&alertmanagerAddress, "alertmanager-address", "http://localhost:9093", "Alertmanager address used to create silences.")
+	flag.StringVar(&alertmanagerTenantId, "alertmanager-default-tenant-id", "", "Alertmanager tenant id.")
+	flag.BoolVar(&alertmanagerAuthentication, "alertmanager-authentication", false, "Enable Alertmanager authentication using Service Account token.")
+
+	opts := zap.Options{
+		Development: false,
+	}
+	opts.BindFlags(flag.CommandLine)
+	flag.Parse()
+
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
+	// if the enable-http2 flag is false (the default), http/2 should be disabled
+	// due to its vulnerabilities. More specifically, disabling http/2 will
+	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
+	// Rapid Reset CVEs. For more information see:
+	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
+	// - https://github.com/advisories/GHSA-4374-p667-p6c8
+	disableHTTP2 := func(c *tls.Config) {
+		setupLog.Info("disabling http/2")
+		c.NextProtos = []string{"http/1.1"}
+	}
+
+	if !enableHTTP2 {
+		tlsOpts = append(tlsOpts, disableHTTP2)
+	}
+
+	// Create watchers for metrics and webhooks certificates
+	var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher
+
+	// Initial webhook TLS options
+	webhookTLSOpts := tlsOpts
+
+	if len(webhookCertPath) > 0 {
+		setupLog.Info("Initializing webhook certificate watcher using provided certificates",
+			"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
+
+		var err error
+		webhookCertWatcher, err = certwatcher.New(
+			filepath.Join(webhookCertPath, webhookCertName),
+			filepath.Join(webhookCertPath, webhookCertKey),
+		)
+		if err != nil {
+			setupLog.Error(err, "Failed to initialize webhook certificate watcher")
+			os.Exit(1)
+		}
+
+		webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) {
+			config.GetCertificate = webhookCertWatcher.GetCertificate
+		})
+	}
+
+	webhookServer := webhook.NewServer(webhook.Options{
+		TLSOpts: webhookTLSOpts,
+	})
+
+	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
+	// More info:
+	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/metrics/server
+	// - https://book.kubebuilder.io/reference/metrics.html
+	metricsServerOptions := metricsserver.Options{
+		BindAddress:   metricsAddr,
+		SecureServing: secureMetrics,
+		TLSOpts:       tlsOpts,
+	}
+
+	if secureMetrics {
+		// FilterProvider is used to protect the metrics endpoint with authn/authz.
+		// These configurations ensure that only authorized users and service accounts
+		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
+		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/metrics/filters#WithAuthenticationAndAuthorization
+		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
+	}
+
+	// If the certificate is not specified, controller-runtime will automatically
+	// generate self-signed certificates for the metrics server. While convenient for development and testing,
+	// this setup is not recommended for production.
+	//
+	// TODO(user): If you enable certManager, uncomment the following lines:
+	// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
+	// managed by cert-manager for the metrics server.
+	// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
+	if len(metricsCertPath) > 0 {
+		setupLog.Info("Initializing metrics certificate watcher using provided certificates",
+			"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
+
+		var err error
+		metricsCertWatcher, err = certwatcher.New(
+			filepath.Join(metricsCertPath, metricsCertName),
+			filepath.Join(metricsCertPath, metricsCertKey),
+		)
+		if err != nil {
+			setupLog.Error(err, "to initialize metrics certificate watcher", "error", err)
+			os.Exit(1)
+		}
+
+		metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) {
+			config.GetCertificate = metricsCertWatcher.GetCertificate
+		})
+	}
+
+	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+		Scheme:                 scheme,
+		Metrics:                metricsServerOptions,
+		WebhookServer:          webhookServer,
+		HealthProbeBindAddress: probeAddr,
+		LeaderElection:         enableLeaderElection,
+		LeaderElectionID:       "5573995c.giantswarm.io",
+		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
+		// when the Manager ends. This requires the binary to immediately end when the
+		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
+		// speeds up voluntary leader transitions as the new leader don't have to wait
+		// LeaseDuration time first.
+		//
+		// In the default scaffold provided, the program ends immediately after
+		// the manager stops, so would be fine to enable this option. However,
+		// if you are doing or is intended to do any operation such as perform cleanups
+		// after the manager stops then its usage might be unsafe.
+		// LeaderElectionReleaseOnCancel: true,
+	})
+	if err != nil {
+		setupLog.Error(err, "unable to start manager")
+		os.Exit(1)
+	}
+
+	// TODO Make this more configurable:
+	// - label selector to select silences
+	// - support all alertmanager auth methods
+	// - support multiple tenant (configurable label like observability.giantswarm.io/tenant)
+	var amClient *alertmanager.AlertManager
+	{
+		amConfig := alertmanager.Config{
+			Address:        alertmanagerAddress,
+			Authentication: alertmanagerAuthentication,
+			BearerToken:    mgr.GetConfig().BearerToken,
+			TenantId:       alertmanagerTenantId,
+		}
+
+		amClient, err = alertmanager.New(amConfig)
+		if err != nil {
+			setupLog.Error(err, "unable to setup client", "client", "Alertmanager")
+			os.Exit(1)
+		}
+	}
+
+	if err = (&controller.SilenceReconciler{
+		Client:       mgr.GetClient(),
+		Scheme:       mgr.GetScheme(),
+		Alertmanager: amClient,
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Silence")
+		os.Exit(1)
+	}
+	// +kubebuilder:scaffold:builder
+
+	if metricsCertWatcher != nil {
+		setupLog.Info("Adding metrics certificate watcher to manager")
+		if err := mgr.Add(metricsCertWatcher); err != nil {
+			setupLog.Error(err, "unable to add metrics certificate watcher to manager")
+			os.Exit(1)
+		}
+	}
+
+	if webhookCertWatcher != nil {
+		setupLog.Info("Adding webhook certificate watcher to manager")
+		if err := mgr.Add(webhookCertWatcher); err != nil {
+			setupLog.Error(err, "unable to add webhook certificate watcher to manager")
+			os.Exit(1)
+		}
+	}
+
+	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
+		setupLog.Error(err, "unable to set up health check")
+		os.Exit(1)
+	}
+	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
+		setupLog.Error(err, "unable to set up ready check")
+		os.Exit(1)
+	}
+
+	setupLog.Info("starting manager")
+	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+		setupLog.Error(err, "problem running manager")
+		os.Exit(1)
+	}
+}
diff --git a/config/crd/bases/monitoring.giantswarm.io_silences.yaml b/config/crd/monitoring.giantswarm.io_silences.yaml
similarity index 100%
rename from config/crd/bases/monitoring.giantswarm.io_silences.yaml
rename to config/crd/monitoring.giantswarm.io_silences.yaml
diff --git a/go.mod b/go.mod
index c7a1ba93..3fb9e359 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,9 @@ require (
 	github.com/giantswarm/micrologger v1.1.2
 	github.com/giantswarm/operatorkit/v7 v7.3.0
 	github.com/google/go-cmp v0.7.0
+	github.com/onsi/ginkgo/v2 v2.22.0
+	github.com/onsi/gomega v1.36.1
+	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.22.0
 	github.com/spf13/viper v1.20.1
 	k8s.io/apimachinery v0.33.0
@@ -21,13 +24,18 @@ require (
 )
 
 require (
+	cel.dev/expr v0.18.0 // indirect
+	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/blang/semver/v4 v4.0.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-semver v0.3.1 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.8.0 // indirect
 	github.com/getsentry/sentry-go v0.32.0 // indirect
@@ -38,17 +46,22 @@ require (
 	github.com/go-kit/log v0.2.1 // indirect
 	github.com/go-logfmt/logfmt v0.6.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.1 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.1 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
+	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/google/btree v1.1.3 // indirect
+	github.com/google/cel-go v0.22.0 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
+	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/mux v1.8.1 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -58,7 +71,6 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.63.0 // indirect
 	github.com/prometheus/procfs v0.16.1 // indirect
@@ -68,10 +80,20 @@ require (
 	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/stoewer/go-strcase v1.3.0 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
+	go.opentelemetry.io/otel v1.28.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
+	go.opentelemetry.io/otel/metric v1.28.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.28.0 // indirect
+	go.opentelemetry.io/otel/trace v1.28.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.3.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
 	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/oauth2 v0.29.0 // indirect
 	golang.org/x/sync v0.13.0 // indirect
@@ -81,6 +103,9 @@ require (
 	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.31.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
+	google.golang.org/grpc v1.65.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
@@ -88,9 +113,12 @@ require (
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/api v0.33.0 // indirect
 	k8s.io/apiextensions-apiserver v0.32.4 // indirect
+	k8s.io/apiserver v0.32.4 // indirect
+	k8s.io/component-base v0.32.4 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
 	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
+	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
 	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
 	sigs.k8s.io/randfill v1.0.0 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
@@ -99,6 +127,5 @@ require (
 
 replace (
 	github.com/dgrijalva/jwt-go => github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
-	github.com/nats-io/nats-server/v2 => github.com/nats-io/nats-server/v2 v2.11.2
 	github.com/spf13/viper => github.com/spf13/viper v1.20.1
 )
diff --git a/go.sum b/go.sum
index 3be76a41..3874686f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,13 @@
+cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
+cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -17,6 +25,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv
 github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
 github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -53,8 +63,11 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
 github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
 github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
 github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
@@ -73,6 +86,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g=
+github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
 github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
 github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -87,6 +102,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -152,10 +169,17 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
 github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -164,6 +188,22 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -173,6 +213,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -214,6 +256,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
+google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -225,6 +273,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
@@ -233,14 +282,20 @@ k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBsk
 k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
 k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
 k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
+k8s.io/apiserver v0.32.4 h1:Yf7sd/y+GOQKH1Qf6wUeayZrYXe2SKZ17Bcq7VQM5HQ=
+k8s.io/apiserver v0.32.4/go.mod h1:JFUMNtE2M5yqLZpIsgCb06SkVSW1YcxW1oyLSTfjXR8=
 k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
 k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
+k8s.io/component-base v0.32.4 h1:HuF+2JVLbFS5GODLIfPCb1Td6b+G2HszJoArcWOSr5I=
+k8s.io/component-base v0.32.4/go.mod h1:10KloJEYw1keU/Xmjfy9TKJqUq7J2mYdiD1VDXoco4o=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
 k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
 k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
 sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
 sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
diff --git a/hack/.gitignore b/hack/.gitignore
index e660fd93..ed3d0007 100644
--- a/hack/.gitignore
+++ b/hack/.gitignore
@@ -1 +1,2 @@
-bin/
+bin/*
+!bin/*.sh
diff --git a/helm/silence-operator/Chart.yaml b/helm/silence-operator/Chart.yaml
index ea50ff7f..2f7a4666 100644
--- a/helm/silence-operator/Chart.yaml
+++ b/helm/silence-operator/Chart.yaml
@@ -1,9 +1,10 @@
-apiVersion: "v2"
-appVersion: "[[ .AppVersion ]]"
-description: "Chart holding silence-operator."
-home: "https://github.com/giantswarm/silence-operator"
-name: "silence-operator"
-version: "[[ .Version ]]"
+apiVersion: v2
+appVersion: 0.14.1
+description: The silence-operator manages alertmanager silences.
+home: https://github.com/giantswarm/silence-operator
+icon: https://s.giantswarm.io/app-icons/giantswarm/1/light.svg
+name: silence-operator
+version: 0.14.1
 annotations:
   application.giantswarm.io/team: "atlas"
   config.giantswarm.io/version: 1.x.x
diff --git a/helm/silence-operator/templates/_helpers.tpl b/helm/silence-operator/templates/_helpers.tpl
index b87d7e0b..bd3d81f6 100644
--- a/helm/silence-operator/templates/_helpers.tpl
+++ b/helm/silence-operator/templates/_helpers.tpl
@@ -19,11 +19,9 @@ Common labels
 {{- define "labels.common" -}}
 app: {{ include "name" . | quote }}
 {{ include "labels.selector" . }}
-application.giantswarm.io/branch: {{ .Values.project.branch | quote }}
-application.giantswarm.io/commit: {{ .Values.project.commit | quote }}
 app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
 app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
-application.giantswarm.io/team: {{ index .Chart.Annotations "application.giantswarm.io/team" | quote }}
+application.giantswarm.io/team: {{ index .Chart.Annotations "application.giantswarm.io/team"  | default "atlas" | quote }}
 helm.sh/chart: {{ include "chart" . | quote }}
 {{- end -}}
 
diff --git a/helm/silence-operator/templates/_resource.tpl b/helm/silence-operator/templates/_resource.tpl
index 6e664847..580e6125 100644
--- a/helm/silence-operator/templates/_resource.tpl
+++ b/helm/silence-operator/templates/_resource.tpl
@@ -12,13 +12,5 @@ room for such suffix.
 {{- end -}}
 
 {{- define "resource.default.namespace" -}}
-{{ .Release.Namespace }}
-{{- end -}}
-
-{{- define "resource.psp.name" -}}
-{{- include "resource.default.name" . -}}-psp
-{{- end -}}
-
-{{- define "resource.pullSecret.name" -}}
-{{- include "resource.default.name" . -}}-pull-secret
+monitoring
 {{- end -}}
diff --git a/helm/silence-operator/templates/configmap.yaml b/helm/silence-operator/templates/configmap.yaml
deleted file mode 100644
index b11db7d0..00000000
--- a/helm/silence-operator/templates/configmap.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: {{ include "resource.default.name"  . }}
-  namespace: {{ include "resource.default.namespace"  . }}
-  labels:
-    {{- include "labels.common" . | nindent 4 }}
-data:
-  config.yml: |
-    server:
-      enable:
-        debug:
-          server: true
-      listen:
-        address: 'http://0.0.0.0:8000'
-    service:
-      alertmanager:
-        address: {{ .Values.alertmanagerAddress }}
-        authentication: {{ .Values.alertmanagerAuthentication }}
-        {{ with .Values.alertmanagerDefaultTenant }}
-        tenantId: {{ . }}
-        {{ end }}
-      kubernetes:
-        address: ''
-        inCluster: true
-        tls:
-          caFile: ''
-          crtFile: ''
-          keyFile: ''
diff --git a/helm/silence-operator/templates/deployment.yaml b/helm/silence-operator/templates/deployment.yaml
index 1540a69a..d5744ee8 100644
--- a/helm/silence-operator/templates/deployment.yaml
+++ b/helm/silence-operator/templates/deployment.yaml
@@ -1,10 +1,10 @@
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: {{ include "resource.default.name"  . }}
-  namespace: {{ include "resource.default.namespace"  . }}
   labels:
     {{- include "labels.common" . | nindent 4 }}
+  name: {{ include "resource.default.name" . }}
+  namespace: {{ include "resource.default.namespace" . }}
 spec:
   replicas: 1
   selector:
@@ -30,27 +30,32 @@ spec:
             weight: 100
       containers:
       - name: {{ include "name" . }}
-        image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
+        image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ default .Chart.Version .Values.image.tag }}"
         args:
-        - daemon
-        - --config.dirs=/var/run/{{ include "name" . }}/configmap/
-        - --config.files=config
-        ports:
-        - name: http
-          containerPort: 8000
-        volumeMounts:
-        - name: {{ include "name" . }}-configmap
-          mountPath: /var/run/{{ include "name" . }}/configmap/
+        - --leader-elect
+        - --alertmanager-address={{ .Values.alertmanagerAddress }}
+        - --alertmanager-authentication={{ .Values.alertmanagerAuthentication }}
+        {{ with .Values.alertmanagerDefaultTenant }}
+        - --alertmanager-default-tenant-id={{ . }}
+        {{ end }}
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 8000
+            port: http-healthz
           initialDelaySeconds: 30
           timeoutSeconds: 1
-        resources:
-          requests:
-            cpu: 100m
-            memory: 220Mi
+        securityContext:
+          {{- with .Values.containerSecurityContext }}
+            {{- . | toYaml | nindent 10 }}
+          {{- end }}
+        ports:
+        - containerPort: 8080
+          name: http
+          protocol: TCP
+        - containerPort: 8081
+          name: http-healthz
+          protocol: TCP
+        resources: {{ toYaml .Values.resources | nindent 10 }}
         securityContext:
           {{- with .Values.containerSecurityContext }}
             {{- . | toYaml | nindent 10 }}
@@ -60,10 +65,3 @@ spec:
           {{- . | toYaml | nindent 8 }}
         {{- end }}
       serviceAccountName: {{ include "resource.default.name" . }}
-      volumes:
-      - name: {{ include "name" . }}-configmap
-        configMap:
-          name: {{ include "resource.default.name"  . }}
-          items:
-          - key: config.yml
-            path: config.yml
diff --git a/helm/silence-operator/templates/network-policy.yaml b/helm/silence-operator/templates/network-policy.yaml
index 293ea16a..140c1dba 100644
--- a/helm/silence-operator/templates/network-policy.yaml
+++ b/helm/silence-operator/templates/network-policy.yaml
@@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
 apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
 metadata:
-  name: {{ include "resource.default.name"  . }}
-  namespace: {{ include "resource.default.namespace"  . }}
   labels:
     {{- include "labels.common" . | nindent 4 }}
+  name: {{ include "resource.default.name" . }}
+  namespace: {{ include "resource.default.namespace" . }}
 spec:
   podSelector:
     matchLabels:
diff --git a/helm/silence-operator/templates/pod-monitor.yaml b/helm/silence-operator/templates/pod-monitor.yaml
index ce7377a6..b0ad1a6f 100644
--- a/helm/silence-operator/templates/pod-monitor.yaml
+++ b/helm/silence-operator/templates/pod-monitor.yaml
@@ -1,10 +1,10 @@
 apiVersion: monitoring.coreos.com/v1
 kind: PodMonitor
 metadata:
-  name: {{ include "resource.default.name" . }}
-  namespace: {{ include "resource.default.namespace" . }}
   labels:
     {{- include "labels.common" . | nindent 4 }}
+  name: {{ include "resource.default.name" . }}
+  namespace: {{ include "resource.default.namespace" . }}
 spec:
   namespaceSelector:
     matchNames:
diff --git a/helm/silence-operator/templates/psp.yaml b/helm/silence-operator/templates/psp.yaml
deleted file mode 100644
index f195f8fa..00000000
--- a/helm/silence-operator/templates/psp.yaml
+++ /dev/null
@@ -1,38 +0,0 @@
-{{- if not .Values.global.podSecurityStandards.enforced }}
-{{- if and .Values.rbac.pspEnabled (.Capabilities.APIVersions.Has "policy/v1beta1") }}
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: {{ include "resource.psp.name" . }}
-  labels:
-    {{- include "labels.common" . | nindent 4 }}
-  annotations:
-    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default'
-spec:
-  privileged: false
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-      - min: 1
-        max: 65535
-  runAsUser:
-    rule: MustRunAsNonRoot
-  runAsGroup:
-    rule: MustRunAs
-    ranges:
-      - min: 1
-        max: 65535
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-    - 'configMap'
-    - 'emptyDir'
-    - 'secret'
-  allowPrivilegeEscalation: false
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-{{- end }}
-{{- end }}
diff --git a/helm/silence-operator/templates/rbac.yaml b/helm/silence-operator/templates/rbac.yaml
index 5ee66727..aa78f1f4 100644
--- a/helm/silence-operator/templates/rbac.yaml
+++ b/helm/silence-operator/templates/rbac.yaml
@@ -2,9 +2,9 @@
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  name: {{ include "resource.default.name"  . }}
   labels:
     {{- include "labels.common" . | nindent 4 }}
+  name: {{ include "resource.default.name" . }}
 rules:
   - apiGroups:
       - "monitoring.giantswarm.io"
@@ -12,54 +12,31 @@ rules:
       - silences
     verbs:
       - "*"
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  name: {{ include "resource.default.name"  . }}
-  labels:
-    {{- include "labels.common" . | nindent 4 }}
-subjects:
-  - kind: ServiceAccount
-    name: {{ include "resource.default.name"  . }}
-    namespace: {{ include "resource.default.namespace"  . }}
-roleRef:
-  kind: ClusterRole
-  name: {{ include "resource.default.name"  . }}
-  apiGroup: rbac.authorization.k8s.io
-{{- if not .Values.global.podSecurityStandards.enforced }}
-{{- if and .Values.rbac.pspEnabled (.Capabilities.APIVersions.Has "policy/v1beta1") }}
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: {{ include "resource.psp.name" . }}
-  labels:
-    {{- include "labels.common" . | nindent 4 }}
-rules:
   - apiGroups:
-      - extensions
+      - coordination.k8s.io
     resources:
-      - podsecuritypolicies
+      - leases
+    verbs: 
+      - "*"
+  - apiGroups:
+      - ""
+    resources:
+      - events
     verbs:
-      - use
-    resourceNames:
-      - {{ include "resource.psp.name" . }}
+      - create
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  name: {{ include "resource.psp.name" . }}
   labels:
     {{- include "labels.common" . | nindent 4 }}
+  name: {{ include "resource.default.name" . }}
 subjects:
   - kind: ServiceAccount
-    name: {{ include "resource.default.name"  . }}
-    namespace: {{ include "resource.default.namespace"  . }}
+    name: {{ include "resource.default.name" . }}
+    namespace: {{ include "resource.default.namespace" . }}
 roleRef:
   kind: ClusterRole
-  name: {{ include "resource.psp.name" . }}
+  name: {{ include "resource.default.name" . }}
   apiGroup: rbac.authorization.k8s.io
 {{- end }}
-{{- end }}
-{{- end }}
diff --git a/helm/silence-operator/templates/service-account.yaml b/helm/silence-operator/templates/service-account.yaml
index 9a905044..738e5e54 100644
--- a/helm/silence-operator/templates/service-account.yaml
+++ b/helm/silence-operator/templates/service-account.yaml
@@ -1,7 +1,7 @@
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  name: {{ include "resource.default.name"  . }}
-  namespace: {{ include "resource.default.namespace"  . }}
   labels:
     {{- include "labels.common" . | nindent 4 }}
+  name: {{ include "resource.default.name" . }}
+  namespace: {{ include "resource.default.namespace" . }}
diff --git a/helm/silence-operator/values.schema.json b/helm/silence-operator/values.schema.json
index 2bdb2748..2137e6a3 100644
--- a/helm/silence-operator/values.schema.json
+++ b/helm/silence-operator/values.schema.json
@@ -11,22 +11,6 @@
         "alertmanagerDefaultTenant": {
             "type": "string"
         },
-        "cluster": {
-            "type": "object",
-            "properties": {
-                "name": {
-                    "type": "string"
-                },
-                "provider": {
-                    "type": "object",
-                    "properties": {
-                        "kind": {
-                            "type": "string"
-                        }
-                    }
-                }
-            }
-        },
         "containerSecurityContext": {
             "type": "object",
             "properties": {
@@ -54,25 +38,15 @@
                 }
             }
         },
-        "global": {
-            "type": "object",
-            "properties": {
-                "podSecurityStandards": {
-                    "type": "object",
-                    "properties": {
-                        "enforced": {
-                            "type": "boolean"
-                        }
-                    }
-                }
-            }
-        },
         "image": {
             "type": "object",
             "properties": {
                 "name": {
                     "type": "string"
                 },
+                "registry": {
+                    "type": "string"
+                },
                 "tag": {
                     "type": "string"
                 }
@@ -100,33 +74,27 @@
                 }
             }
         },
-        "project": {
-            "type": "object",
-            "properties": {
-                "branch": {
-                    "type": "string"
-                },
-                "commit": {
-                    "type": "string"
-                }
-            }
-        },
         "rbac": {
             "type": "object",
             "properties": {
                 "create": {
                     "type": "boolean"
-                },
-                "pspEnabled": {
-                    "type": "boolean"
                 }
             }
         },
-        "registry": {
+        "resources": {
             "type": "object",
             "properties": {
-                "domain": {
-                    "type": "string"
+                "requests": {
+                    "type": "object",
+                    "properties": {
+                        "cpu": {
+                            "type": "string"
+                        },
+                        "memory": {
+                            "type": "string"
+                        }
+                    }
                 }
             }
         }
diff --git a/helm/silence-operator/values.yaml b/helm/silence-operator/values.yaml
index 1ceeedbe..33a8c7da 100644
--- a/helm/silence-operator/values.yaml
+++ b/helm/silence-operator/values.yaml
@@ -1,46 +1,38 @@
-global:
-  podSecurityStandards:
-    enforced: false
-
 image:
+  registry: gsoci.azurecr.io
   name: "giantswarm/silence-operator"
-  tag: "[[ .Version ]]"
+  tag: ""
 
+# TODO improve this for better user experience
 alertmanagerAddress: ""
 alertmanagerAuthentication: false
 # -- Default alertmanager tenant
 alertmanagerDefaultTenant: ""
 
-project:
-  branch: "[[ .Branch ]]"
-  commit: "[[ .SHA ]]"
-
-cluster:
-  name: ""
-  provider:
-    kind: ""
-
-registry:
-  domain: gsoci.azurecr.io
-
-# Add seccomp to pod security context
+# -- Configures the pod security context
 podSecurityContext:
-  runAsGroup: 1000
-  runAsUser: 1000
   runAsNonRoot: true
+  runAsUser: 65534
   seccompProfile:
     type: RuntimeDefault
 
-# Add seccomp to container security context
+# -- Configures the container security context
 containerSecurityContext:
   allowPrivilegeEscalation: false
-  capabilities:
-    drop:
-      - "ALL"
+  readOnlyRootFilesystem: true
   seccompProfile:
     type: RuntimeDefault
+  capabilities:
+    drop:
+      - ALL
+
+resources:
+  requests:
+    cpu: 100m
+    memory: 220Mi
+  limits:
+    cpu: 200m
+    memory: 300Mi
 
 rbac:
   create: true
-  # The PSP will only be deployed when Kubernetes supports it (<1.25)
-  pspEnabled: true
diff --git a/internal/controller/silence_controller.go b/internal/controller/silence_controller.go
new file mode 100644
index 00000000..75f46cb7
--- /dev/null
+++ b/internal/controller/silence_controller.go
@@ -0,0 +1,191 @@
+/*
+Copyright 2025.
+
+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"
+	"reflect"
+	"time"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/giantswarm/microerror"
+	"github.com/pkg/errors"
+
+	"github.com/giantswarm/silence-operator/api/v1alpha1"
+	"github.com/giantswarm/silence-operator/pkg/alertmanager"
+	"github.com/giantswarm/silence-operator/service/controller/key"
+)
+
+// SilenceReconciler reconciles a Silence object
+type SilenceReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+
+	Alertmanager *alertmanager.AlertManager
+}
+
+// +kubebuilder:rbac:groups=monitoring.giantswarm.io,resources=silences,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=monitoring.giantswarm.io,resources=silences/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=monitoring.giantswarm.io,resources=silences/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.20.4/pkg/reconcile
+func (r *SilenceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	logger := log.FromContext(ctx)
+
+	logger.Info("Started reconciling silence")
+	defer logger.Info("Finished reconciling silence")
+
+	silence := &v1alpha1.Silence{}
+	err := r.Get(ctx, req.NamespacedName, silence)
+	if err != nil {
+		return ctrl.Result{}, errors.WithStack(client.IgnoreNotFound(err))
+	}
+
+	if !silence.DeletionTimestamp.IsZero() {
+		return ctrl.Result{}, r.reconcileDelete(ctx, silence)
+	}
+
+	// TODO add finalizer to silence CRs to prevent deletion of silences
+
+	return r.reconcileCreate(ctx, silence)
+}
+
+// TODO encapsulate business logic in a separate package
+func (r *SilenceReconciler) reconcileCreate(ctx context.Context, silence *v1alpha1.Silence) (ctrl.Result, error) {
+	logger := log.FromContext(ctx)
+	newSilence, err := getSilenceFromCR(silence)
+	if err != nil {
+		return ctrl.Result{}, errors.WithStack(err)
+	}
+
+	now := time.Now()
+
+	var existingSilence *alertmanager.Silence
+	existingSilence, err = r.Alertmanager.GetSilenceByComment(key.SilenceComment(silence))
+	notFound := alertmanager.IsNotFound(err)
+	if !notFound && err != nil {
+		return ctrl.Result{}, errors.WithStack(err)
+	}
+	if notFound {
+		if newSilence.EndsAt.After(now) {
+			logger.Info("creating silence")
+
+			err = r.Alertmanager.CreateSilence(newSilence)
+			if err != nil {
+				return ctrl.Result{}, errors.WithStack(err)
+			}
+			logger.Info("created silence")
+		} else {
+			logger.Info("skipped creation : silence is expired")
+		}
+	} else if newSilence.EndsAt.Before(now) {
+		logger.Info("deleting silence")
+
+		err = r.Alertmanager.DeleteSilenceByID(existingSilence.ID)
+		if err != nil {
+			return ctrl.Result{}, errors.WithStack(err)
+		}
+		logger.Info("deleted silence")
+	} else if updateNeeded(existingSilence, newSilence) {
+		newSilence.ID = existingSilence.ID
+		logger.Info("updating silence")
+
+		err = r.Alertmanager.UpdateSilence(newSilence)
+		if err != nil {
+			return ctrl.Result{}, errors.WithStack(err)
+		}
+		logger.Info("updated silence")
+	} else {
+		logger.Info("skipped update : silence unchanged")
+	}
+
+	return ctrl.Result{}, nil
+}
+
+func (r *SilenceReconciler) reconcileDelete(ctx context.Context, silence *v1alpha1.Silence) error {
+	logger := log.FromContext(ctx)
+	logger.Info("deleting silence")
+
+	err := r.Alertmanager.DeleteSilenceByComment(key.SilenceComment(silence))
+	if err != nil {
+		if alertmanager.IsNotFound(err) {
+			logger.Info("silence does not exist")
+			return nil
+		}
+		return errors.WithStack(err)
+	}
+
+	logger.Info("silence has been deleted")
+
+	return nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *SilenceReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&v1alpha1.Silence{}).
+		Named("silence").
+		Complete(r)
+}
+
+func getSilenceFromCR(silence *v1alpha1.Silence) (*alertmanager.Silence, error) {
+	var matchers []alertmanager.Matcher
+	{
+		for _, matcher := range silence.Spec.Matchers {
+			isEqual := true
+			if matcher.IsEqual != nil {
+				isEqual = *matcher.IsEqual
+			}
+			newMatcher := alertmanager.Matcher{
+				IsEqual: isEqual,
+				IsRegex: matcher.IsRegex,
+				Name:    matcher.Name,
+				Value:   matcher.Value,
+			}
+			matchers = append(matchers, newMatcher)
+		}
+	}
+
+	endsAt, err := key.SilenceEndsAt(silence)
+	if err != nil {
+		return nil, microerror.Mask(err)
+	}
+
+	newSilence := &alertmanager.Silence{
+		Comment:   key.SilenceComment(silence),
+		CreatedBy: key.CreatedBy,
+		StartsAt:  silence.GetCreationTimestamp().Time,
+		EndsAt:    endsAt,
+		Matchers:  matchers,
+	}
+
+	return newSilence, nil
+}
+
+// updateNeeded return true when silence need to be updated.
+func updateNeeded(existingSilence, newSilence *alertmanager.Silence) bool {
+	return !reflect.DeepEqual(existingSilence.Matchers, newSilence.Matchers) ||
+		!existingSilence.EndsAt.Equal(newSilence.EndsAt)
+}
diff --git a/internal/controller/silence_controller_test.go b/internal/controller/silence_controller_test.go
new file mode 100644
index 00000000..1576021e
--- /dev/null
+++ b/internal/controller/silence_controller_test.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2025.
+
+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"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	monitoringv1alpha1 "github.com/giantswarm/silence-operator/api/v1alpha1"
+)
+
+var _ = Describe("Silence Controller", func() {
+	Context("When reconciling a resource", func() {
+		const resourceName = "test-resource"
+
+		ctx := context.Background()
+
+		typeNamespacedName := types.NamespacedName{
+			Name:      resourceName,
+			Namespace: "default", // TODO(user):Modify as needed
+		}
+		silence := &monitoringv1alpha1.Silence{}
+
+		BeforeEach(func() {
+			By("creating the custom resource for the Kind Silence")
+			err := k8sClient.Get(ctx, typeNamespacedName, silence)
+			if err != nil && errors.IsNotFound(err) {
+				resource := &monitoringv1alpha1.Silence{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      resourceName,
+						Namespace: "default",
+					},
+					// TODO(user): Specify other spec details if needed.
+				}
+				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
+			}
+		})
+
+		AfterEach(func() {
+			// TODO(user): Cleanup logic after each test, like removing the resource instance.
+			resource := &monitoringv1alpha1.Silence{}
+			err := k8sClient.Get(ctx, typeNamespacedName, resource)
+			Expect(err).NotTo(HaveOccurred())
+
+			By("Cleanup the specific resource instance Silence")
+			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
+		})
+		It("should successfully reconcile the resource", func() {
+			By("Reconciling the created resource")
+			controllerReconciler := &SilenceReconciler{
+				Client: k8sClient,
+				Scheme: k8sClient.Scheme(),
+			}
+
+			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
+				NamespacedName: typeNamespacedName,
+			})
+			Expect(err).NotTo(HaveOccurred())
+			// 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.
+		})
+	})
+})
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
new file mode 100644
index 00000000..734c13ad
--- /dev/null
+++ b/internal/controller/suite_test.go
@@ -0,0 +1,129 @@
+/*
+Copyright 2025.
+
+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"
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	monitoringv1alpha1 "github.com/giantswarm/silence-operator/api/v1alpha1"
+	// +kubebuilder:scaffold:imports
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+	ctx       context.Context
+	cancel    context.CancelFunc
+	testEnv   *envtest.Environment
+	cfg       *rest.Config
+	k8sClient client.Client
+)
+
+func TestControllers(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecs(t, "Controller Suite")
+}
+
+func getEnvOrSkip(env string) string {
+	value := os.Getenv(env)
+	if value == "" {
+		Skip(fmt.Sprintf("%s not exported", env))
+	}
+
+	return value
+}
+
+var _ = BeforeSuite(func() {
+	getEnvOrSkip("KUBEBUILDER_ASSETS")
+
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+	ctx, cancel = context.WithCancel(context.TODO())
+
+	var err error
+	err = monitoringv1alpha1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	// +kubebuilder:scaffold:scheme
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
+		ErrorIfCRDPathMissing: true,
+	}
+
+	// Retrieve the first found binary directory to allow running tests from IDEs
+	if getFirstFoundEnvTestBinaryDir() != "" {
+		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
+	}
+
+	// cfg is defined in this file globally.
+	cfg, err = testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(k8sClient).NotTo(BeNil())
+})
+
+var _ = AfterSuite(func() {
+	getEnvOrSkip("KUBEBUILDER_ASSETS")
+	By("tearing down the test environment")
+	cancel()
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
+
+// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
+// ENVTEST-based tests depend on specific binaries, usually located in paths set by
+// controller-runtime. When running tests directly (e.g., via an IDE) without using
+// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
+//
+// This function streamlines the process by finding the required binaries, similar to
+// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
+// properly set up, run 'make setup-envtest' beforehand.
+func getFirstFoundEnvTestBinaryDir() string {
+	basePath := filepath.Join("..", "..", "bin", "k8s")
+	entries, err := os.ReadDir(basePath)
+	if err != nil {
+		logf.Log.Error(err, "Failed to read directory", "path", basePath)
+		return ""
+	}
+	for _, entry := range entries {
+		if entry.IsDir() {
+			return filepath.Join(basePath, entry.Name())
+		}
+	}
+	return ""
+}
diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go
index a55ffe46..f8a45a83 100644
--- a/pkg/alertmanager/alertmanager.go
+++ b/pkg/alertmanager/alertmanager.go
@@ -12,6 +12,7 @@ import (
 	"github.com/giantswarm/silence-operator/service/controller/key"
 )
 
+// TODO Get rid of microerrors and use errors instead.
 type Config struct {
 	Address        string
 	Authentication bool
diff --git a/pkg/alertmanager/types.go b/pkg/alertmanager/types.go
index 7b009002..00aef1a4 100644
--- a/pkg/alertmanager/types.go
+++ b/pkg/alertmanager/types.go
@@ -4,6 +4,7 @@ import (
 	"time"
 )
 
+// TODO Can we use open API Types here instead of defining our own types?
 type Silence struct {
 	Comment   string    `json:"comment"`
 	CreatedBy string    `json:"createdBy"`
diff --git a/service/controller/key/key.go b/service/controller/key/key.go
index cd91352a..881d5059 100644
--- a/service/controller/key/key.go
+++ b/service/controller/key/key.go
@@ -30,7 +30,7 @@ func ToSilence(v interface{}) (v1alpha1.Silence, error) {
 	return *c, nil
 }
 
-func SilenceComment(silence v1alpha1.Silence) string {
+func SilenceComment(silence *v1alpha1.Silence) string {
 	return fmt.Sprintf("%s-%s", CreatedBy, silence.Name)
 }
 
@@ -38,7 +38,7 @@ func SilenceComment(silence v1alpha1.Silence) string {
 // The expiry date is retrieved from the annotation name configured by ValidUntilAnnotationName.
 // The expected format is defined by DateOnlyLayout.
 // It returns an invalidExpirationDateError in case the date format is invalid.
-func SilenceEndsAt(silence v1alpha1.Silence) (time.Time, error) {
+func SilenceEndsAt(silence *v1alpha1.Silence) (time.Time, error) {
 	annotations := silence.GetAnnotations()
 
 	// Check if the annotation exist otherwise return a date 100 years in the future.
diff --git a/service/controller/resource/silence/create.go b/service/controller/resource/silence/create.go
index 40869c13..7f4ca06f 100644
--- a/service/controller/resource/silence/create.go
+++ b/service/controller/resource/silence/create.go
@@ -30,13 +30,13 @@ func (r *Resource) getSilenceFromCR(silence v1alpha1.Silence) (*alertmanager.Sil
 		}
 	}
 
-	endsAt, err := key.SilenceEndsAt(silence)
+	endsAt, err := key.SilenceEndsAt(&silence)
 	if err != nil {
 		return nil, microerror.Mask(err)
 	}
 
 	newSilence := &alertmanager.Silence{
-		Comment:   key.SilenceComment(silence),
+		Comment:   key.SilenceComment(&silence),
 		CreatedBy: key.CreatedBy,
 		StartsAt:  silence.GetCreationTimestamp().Time,
 		EndsAt:    endsAt,
@@ -60,7 +60,7 @@ func (r *Resource) EnsureCreated(ctx context.Context, obj interface{}) error {
 	now := time.Now()
 
 	var existingSilence *alertmanager.Silence
-	existingSilence, err = r.amClient.GetSilenceByComment(key.SilenceComment(silence))
+	existingSilence, err = r.amClient.GetSilenceByComment(key.SilenceComment(&silence))
 	notFound := alertmanager.IsNotFound(err)
 	if !notFound && err != nil {
 		return microerror.Mask(err)
diff --git a/service/controller/resource/silence/delete.go b/service/controller/resource/silence/delete.go
index 6b711a61..7825e76f 100644
--- a/service/controller/resource/silence/delete.go
+++ b/service/controller/resource/silence/delete.go
@@ -17,7 +17,7 @@ func (r *Resource) EnsureDeleted(ctx context.Context, obj interface{}) error {
 
 	r.logger.LogCtx(ctx, "level", "debug", "message", "deleting silence")
 
-	err = r.amClient.DeleteSilenceByComment(key.SilenceComment(silence))
+	err = r.amClient.DeleteSilenceByComment(key.SilenceComment(&silence))
 	if alertmanager.IsNotFound(err) {
 		r.logger.LogCtx(ctx, "level", "debug", "message", "silence does not exist")
 		return nil


Reviews:
user: login: QuantumEnigmaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:25:05Z
user: login: QuentinBisson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T17:09:42Z
user: login: QuentinBisson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T17:10:39Z
user: login: QuentinBisson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T17:12:50Z
user: login: QuentinBisson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T17:13:57Z
user: login: QuantumEnigmaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T08:22:36Z
user: login: QuantumEnigmaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T08:23:23Z
user: login: QuentinBisson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:13:33Z
user: login: QuentinBisson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:14:03Z
user: login: QuantumEnigmaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T10:01:34Z
user: login: hervenicol
body: Wow, that's a lot of changes :sweat_smile: 

I can't review this in depth, but the current process of rewriting most of this operator introduces some breaking changes.
I think we should document them, and next release should be a major alpha to make things clear to external people who may use this operator.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:25:34Z
user: login: QuantumEnigmaa
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T08:03:39Z

Review comments:
diff_hunk: @@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
path: helm/silence-operator/templates/network-policy.yaml
user: login: QuantumEnigmaa
body: Why not adding a ciliumnetpol template directly ?
created_at: 2025-04-30T15:13:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,191 @@
+/*
path: internal/controller/silence_controller.go
user: login: QuantumEnigmaa
body: What is this controller replacing ? Didn't the silence-operator already had the equivalent of this controller ? 

I guess that this is the kube-builder way of doing things but I'd like to know how it was handled before
created_at: 2025-04-30T15:16:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,29 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: {{ include "resource.default.name"  . }}
-  namespace: {{ include "resource.default.namespace"  . }}
-  labels:
-    {{- include "labels.common" . | nindent 4 }}
-data:
-  config.yml: |
-    server:
-      enable:
-        debug:
-          server: true
-      listen:
-        address: 'http://0.0.0.0:8000'
-    service:
-      alertmanager:
path: helm/silence-operator/templates/configmap.yaml
user: login: QuantumEnigmaa
body: I understand that you would get rid of this part since you're directly injecting this into the deployment's image `args` field but is the rest of the data from this configmap also mounted in some way into the deployment ?
created_at: 2025-04-30T15:20:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,27 +30,32 @@ spec:
             weight: 100
       containers:
       - name: {{ include "name" . }}
-        image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
+        image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ default .Chart.Version .Values.image.tag }}"
         args:
-        - daemon
-        - --config.dirs=/var/run/{{ include "name" . }}/configmap/
-        - --config.files=config
-        ports:
-        - name: http
-          containerPort: 8000
-        volumeMounts:
-        - name: {{ include "name" . }}-configmap
-          mountPath: /var/run/{{ include "name" . }}/configmap/
+        - --leader-elect
+        - --alertmanager-address={{ .Values.alertmanagerAddress }}
+        - --alertmanager-authentication={{ .Values.alertmanagerAuthentication }}
+        {{ with .Values.alertmanagerDefaultTenant }}
path: helm/silence-operator/templates/deployment.yaml
user: login: QuantumEnigmaa
body: Are you using `with` in place of `if`  here ? I guess having something like that...

```yaml
{{- if .Values.alertmanagerDefaultTenant }}
- --alertmanager-default-tenant-id={{ .Values.alertmanagerDefaultTenant }}
{{- end }}
```

... Will give the same result  but just to be sure.
created_at: 2025-04-30T15:25:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
path: helm/silence-operator/templates/network-policy.yaml
user: login: QuentinBisson
body: Because we want to have it tout opensource so we need to support both
created_at: 2025-04-30T17:09:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,191 @@
+/*
path: internal/controller/silence_controller.go
user: login: QuentinBisson
body: I've kept the code so it should bé here https://github.com/giantswarm/silence-operator/blob/master/service/controller/silence.go
created_at: 2025-04-30T17:10:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,27 +30,32 @@ spec:
             weight: 100
       containers:
       - name: {{ include "name" . }}
-        image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
+        image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ default .Chart.Version .Values.image.tag }}"
         args:
-        - daemon
-        - --config.dirs=/var/run/{{ include "name" . }}/configmap/
-        - --config.files=config
-        ports:
-        - name: http
-          containerPort: 8000
-        volumeMounts:
-        - name: {{ include "name" . }}-configmap
-          mountPath: /var/run/{{ include "name" . }}/configmap/
+        - --leader-elect
+        - --alertmanager-address={{ .Values.alertmanagerAddress }}
+        - --alertmanager-authentication={{ .Values.alertmanagerAuthentication }}
+        {{ with .Values.alertmanagerDefaultTenant }}
path: helm/silence-operator/templates/deployment.yaml
user: login: QuentinBisson
body: Yes the with just removes the duplication 
created_at: 2025-04-30T17:12:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,29 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: {{ include "resource.default.name"  . }}
-  namespace: {{ include "resource.default.namespace"  . }}
-  labels:
-    {{- include "labels.common" . | nindent 4 }}
-data:
-  config.yml: |
-    server:
-      enable:
-        debug:
-          server: true
-      listen:
-        address: 'http://0.0.0.0:8000'
-    service:
-      alertmanager:
path: helm/silence-operator/templates/configmap.yaml
user: login: QuentinBisson
body: Yes it's in the deployment.yaml and read in cmd/main.go
created_at: 2025-04-30T17:13:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
path: helm/silence-operator/templates/network-policy.yaml
user: login: QuantumEnigmaa
body: Yeah I meant having both the netpol AND ciliumnetpol templates. 

And isn't cilium open source ? :thinking: 
created_at: 2025-05-01T08:22:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,27 +30,32 @@ spec:
             weight: 100
       containers:
       - name: {{ include "name" . }}
-        image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
+        image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ default .Chart.Version .Values.image.tag }}"
         args:
-        - daemon
-        - --config.dirs=/var/run/{{ include "name" . }}/configmap/
-        - --config.files=config
-        ports:
-        - name: http
-          containerPort: 8000
-        volumeMounts:
-        - name: {{ include "name" . }}-configmap
-          mountPath: /var/run/{{ include "name" . }}/configmap/
+        - --leader-elect
+        - --alertmanager-address={{ .Values.alertmanagerAddress }}
+        - --alertmanager-authentication={{ .Values.alertmanagerAuthentication }}
+        {{ with .Values.alertmanagerDefaultTenant }}
path: helm/silence-operator/templates/deployment.yaml
user: login: QuantumEnigmaa
body: Ok thanks, thought so.
created_at: 2025-05-01T08:23:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
path: helm/silence-operator/templates/network-policy.yaml
user: login: QuentinBisson
body: Yes to both but this is already too much changes and I tried to focus on the least amount of changes possible 🤣
created_at: 2025-05-01T09:13:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
path: helm/silence-operator/templates/network-policy.yaml
user: login: QuentinBisson
body: Now, cilium is opensource but the crds are only présent if the cluster is using cilium, not any other cni :)
created_at: 2025-05-01T09:14:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,10 +1,11 @@
-kind: NetworkPolicy
+# TODO add cilium network policy flavor
path: helm/silence-operator/templates/network-policy.yaml
user: login: QuantumEnigmaa
body: Well sure x)
created_at: 2025-05-01T10:01:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: QuentinBisson
created_at: 2025-04-28T21:50:14Z
body: Well it works:

```bash
k logs -n monitoring silence-operator-59f87fd45f-lsglt -f
{"level":"info","ts":"2025-04-28T21:48:13Z","logger":"setup","msg":"starting manager"}
{"level":"info","ts":"2025-04-28T21:48:13Z","msg":"starting server","name":"health probe","addr":"[::]:8081"}
I0428 21:48:13.781642       1 leaderelection.go:257] attempting to acquire leader lease monitoring/5573995c.giantswarm.io...
I0428 21:48:30.512436       1 leaderelection.go:271] successfully acquired lease monitoring/5573995c.giantswarm.io
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Starting EventSource","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","source":"kind source: *v1alpha1.Silence"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Starting Controller","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Starting workers","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","worker count":1}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-karpentercannotregisternewnodes"},"namespace":"","name":"common-karpentercannotregisternewnodes","reconcileID":"602bf6f1-3dc0-4efc-9ff1-e923fd8d0d8b"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped update : silence unchanged","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-karpentercannotregisternewnodes"},"namespace":"","name":"common-karpentercannotregisternewnodes","reconcileID":"602bf6f1-3dc0-4efc-9ff1-e923fd8d0d8b"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-karpentercannotregisternewnodes"},"namespace":"","name":"common-karpentercannotregisternewnodes","reconcileID":"602bf6f1-3dc0-4efc-9ff1-e923fd8d0d8b"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-metricforwardingerrors"},"namespace":"","name":"common-metricforwardingerrors","reconcileID":"d63011ef-5eaa-45ee-b0e6-da148a77272c"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped creation : silence is expired","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-metricforwardingerrors"},"namespace":"","name":"common-metricforwardingerrors","reconcileID":"d63011ef-5eaa-45ee-b0e6-da148a77272c"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-metricforwardingerrors"},"namespace":"","name":"common-metricforwardingerrors","reconcileID":"d63011ef-5eaa-45ee-b0e6-da148a77272c"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"gs-staff-capi-migration-testing"},"namespace":"","name":"gs-staff-capi-migration-testing","reconcileID":"3e2de17e-7769-43e4-bc91-95cc1f684801"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped update : silence unchanged","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"gs-staff-capi-migration-testing"},"namespace":"","name":"gs-staff-capi-migration-testing","reconcileID":"3e2de17e-7769-43e4-bc91-95cc1f684801"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"gs-staff-capi-migration-testing"},"namespace":"","name":"gs-staff-capi-migration-testing","reconcileID":"3e2de17e-7769-43e4-bc91-95cc1f684801"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"gs-staff-stable-testing"},"namespace":"","name":"gs-staff-stable-testing","reconcileID":"b2329661-3a68-4062-b3ae-ef9ccf5c32ca"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped update : silence unchanged","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"gs-staff-stable-testing"},"namespace":"","name":"gs-staff-stable-testing","reconcileID":"b2329661-3a68-4062-b3ae-ef9ccf5c32ca"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"gs-staff-stable-testing"},"namespace":"","name":"gs-staff-stable-testing","reconcileID":"b2329661-3a68-4062-b3ae-ef9ccf5c32ca"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"karpentercannotregisternewnodes"},"namespace":"","name":"karpentercannotregisternewnodes","reconcileID":"5d1938f5-6374-49d7-b230-0fed40b252df"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped update : silence unchanged","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"karpentercannotregisternewnodes"},"namespace":"","name":"karpentercannotregisternewnodes","reconcileID":"5d1938f5-6374-49d7-b230-0fed40b252df"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"karpentercannotregisternewnodes"},"namespace":"","name":"karpentercannotregisternewnodes","reconcileID":"5d1938f5-6374-49d7-b230-0fed40b252df"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"metricforwardingerrors"},"namespace":"","name":"metricforwardingerrors","reconcileID":"204ed507-bf57-4458-865a-5ba2c390b4b5"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped creation : silence is expired","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"metricforwardingerrors"},"namespace":"","name":"metricforwardingerrors","reconcileID":"204ed507-bf57-4458-865a-5ba2c390b4b5"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"metricforwardingerrors"},"namespace":"","name":"metricforwardingerrors","reconcileID":"204ed507-bf57-4458-865a-5ba2c390b4b5"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Started reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-alloy-unhealthy-components"},"namespace":"","name":"common-alloy-unhealthy-components","reconcileID":"fb75c1d7-70a8-4895-9468-e717479ffe99"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"skipped update : silence unchanged","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-alloy-unhealthy-components"},"namespace":"","name":"common-alloy-unhealthy-components","reconcileID":"fb75c1d7-70a8-4895-9468-e717479ffe99"}
{"level":"info","ts":"2025-04-28T21:48:30Z","msg":"Finished reconciling silence","controller":"silence","controllerGroup":"monitoring.giantswarm.io","controllerKind":"Silence","Silence":{"name":"common-alloy-unhealthy-components"},"namespace":"","name":"common-alloy-unhealthy-components","reconcileID":"fb75c1d7-70a8-4895-9468-e717479ffe99"}
```
author_association: CONTRIBUTOR
user: login: QuentinBisson
created_at: 2025-05-02T13:36:58Z
body: The plan is definitely yo make it a major anyway :)

Where do you see breaking changes though? Maybe we can discuss them on Monday :)
author_association: CONTRIBUTOR
user: login: QuentinBisson
created_at: 2025-05-05T08:15:35Z
body: Rebased
author_association: CONTRIBUTOR
user: login: QuentinBisson
created_at: 2025-05-05T08:35:48Z
body: @hervenicol FYI, the list of changes is smaller here. I had to rebase first :)
author_association: CONTRIBUTOR
user: login: hervenicol
created_at: 2025-05-05T09:08:23Z
body: > Where do you see breaking changes though?

The most obvious I've seen is the config moved from a file (provisioned via a CM) to command-line arguments.
The removal of the `sync` feature (in a previous PR) is also a breaking change.
And I probably miss some.
author_association: CONTRIBUTOR
user: login: QuentinBisson
created_at: 2025-05-05T09:08:58Z
body: Sure those make sense :)
author_association: CONTRIBUTOR