diff --git a/Dockerfile.buildx b/Dockerfile.buildx index 5338d7d2..377ecc76 100644 --- a/Dockerfile.buildx +++ b/Dockerfile.buildx @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.24 AS deps +FROM --platform=$BUILDPLATFORM golang:1.25 AS deps ARG TARGETPLATFORM ARG BUILDPLATFORM diff --git a/config.go b/config.go index 424d7b69..a67e30f1 100644 --- a/config.go +++ b/config.go @@ -17,4 +17,5 @@ type flags struct { ReplicateServiceAccounts bool SyncByContent bool ExcludeNamespaces string + ExcludeAnnotations string } diff --git a/main.go b/main.go index 91f3e3bb..5277ff2a 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ func init() { flag.BoolVar(&f.ReplicateServiceAccounts, "replicate-service-accounts", true, "Enable replication of service accounts") flag.BoolVar(&f.SyncByContent, "sync-by-content", false, "Always compare the contents of source and target resources and force them to be the same") flag.StringVar(&f.ExcludeNamespaces, "exclude-namespaces", "", "Comma-separated list of regex patterns for namespaces to exclude from replication") + flag.StringVar(&f.ExcludeAnnotations, "exclude-annotations", "", "Comma-separated list of regex patterns for annotations of resource to exclude from replication") flag.Parse() switch strings.ToUpper(strings.TrimSpace(f.LogLevel)) { @@ -92,32 +93,35 @@ func main() { excludePatterns := strings.Split(f.ExcludeNamespaces, ",") filter := common.NewNamespaceFilter(excludePatterns) + excludeAnnotationsPatterns := strings.Split(f.ExcludeAnnotations, ",") + annotationsFilter := common.NewAnnotationsFilter(excludeAnnotationsPatterns) + if f.ReplicateSecrets { - secretRepl := secret.NewReplicator(client, f.ResyncPeriod, f.AllowAll, f.SyncByContent, filter) + secretRepl := secret.NewReplicator(client, f.ResyncPeriod, f.AllowAll, f.SyncByContent, filter, annotationsFilter) go secretRepl.Run() enabledReplicators = append(enabledReplicators, secretRepl) } if f.ReplicateConfigMaps { - configMapRepl := configmap.NewReplicator(client, f.ResyncPeriod, f.AllowAll, f.SyncByContent, filter) + configMapRepl := configmap.NewReplicator(client, f.ResyncPeriod, f.AllowAll, f.SyncByContent, filter, annotationsFilter) go configMapRepl.Run() enabledReplicators = append(enabledReplicators, configMapRepl) } if f.ReplicateRoles { - roleRepl := role.NewReplicator(client, f.ResyncPeriod, f.AllowAll) + roleRepl := role.NewReplicator(client, f.ResyncPeriod, f.AllowAll, annotationsFilter) go roleRepl.Run() enabledReplicators = append(enabledReplicators, roleRepl) } if f.ReplicateRoleBindings { - roleBindingRepl := rolebinding.NewReplicator(client, f.ResyncPeriod, f.AllowAll) + roleBindingRepl := rolebinding.NewReplicator(client, f.ResyncPeriod, f.AllowAll, annotationsFilter) go roleBindingRepl.Run() enabledReplicators = append(enabledReplicators, roleBindingRepl) } if f.ReplicateServiceAccounts { - serviceAccountRepl := serviceaccount.NewReplicator(client, f.ResyncPeriod, f.AllowAll) + serviceAccountRepl := serviceaccount.NewReplicator(client, f.ResyncPeriod, f.AllowAll, annotationsFilter) go serviceAccountRepl.Run() enabledReplicators = append(enabledReplicators, serviceAccountRepl) } diff --git a/replicate/common/annotations_exclude.go b/replicate/common/annotations_exclude.go new file mode 100644 index 00000000..d7a836ed --- /dev/null +++ b/replicate/common/annotations_exclude.go @@ -0,0 +1,38 @@ +package common + +import ( + "regexp" +) + +type AnnotationsFilter struct { + ExcludePatterns []string + compiled []*regexp.Regexp +} + +func NewAnnotationsFilter(patterns []string) *AnnotationsFilter { + var compiled []*regexp.Regexp + for _, pat := range patterns { + if pat == "" { + continue + } + re, err := regexp.Compile(pat) + if err == nil { + compiled = append(compiled, re) + } + } + return &AnnotationsFilter{ + ExcludePatterns: patterns, + compiled: compiled, + } +} + +func (f *AnnotationsFilter) ShouldExclude(annotations map[string]string) bool { + for _, re := range f.compiled { + for annotation := range annotations { + if re.MatchString(annotation) { + return true + } + } + } + return false +} diff --git a/replicate/common/annotations_exclude_test.go b/replicate/common/annotations_exclude_test.go new file mode 100644 index 00000000..7d201af3 --- /dev/null +++ b/replicate/common/annotations_exclude_test.go @@ -0,0 +1,53 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAnnotationsFilterShouldExclude(t *testing.T) { + tests := []struct { + patterns []string + annotations map[string]string + expectedRes bool + }{ + { + patterns: []string{"vcluster.loft.sh/"}, + annotations: map[string]string{ + "vcluster.loft.sh/any1": "any1", + "vcluster.loft.sh/any2": "any2", + }, + expectedRes: true, + }, + { + patterns: []string{"vcluster.loft.sh/"}, + annotations: map[string]string{ + "any1": "any1", + "any2": "any2", + }, + expectedRes: false, + }, + { + patterns: []string{}, + annotations: map[string]string{ + "any1": "any1", + "any2": "any2", + }, + expectedRes: false, + }, + { + patterns: []string{"vcluster.loft.sh/"}, + annotations: map[string]string{}, + expectedRes: false, + }, + } + + for _, test := range tests { + annotationsFilter := NewAnnotationsFilter(test.patterns) + + res := annotationsFilter.ShouldExclude(test.annotations) + + assert.Equal(t, test.expectedRes, res) + } +} diff --git a/replicate/common/generic-replicator.go b/replicate/common/generic-replicator.go index 34859b84..7342934d 100644 --- a/replicate/common/generic-replicator.go +++ b/replicate/common/generic-replicator.go @@ -23,15 +23,16 @@ import ( ) type ReplicatorConfig struct { - Kind string - Client kubernetes.Interface - ResyncPeriod time.Duration - AllowAll bool - SyncByContent bool - ListFunc cache.ListFunc - WatchFunc cache.WatchFunc - ObjType runtime.Object - NamespaceFilter *NamespaceFilter + Kind string + Client kubernetes.Interface + ResyncPeriod time.Duration + AllowAll bool + SyncByContent bool + ListFunc cache.ListFunc + WatchFunc cache.WatchFunc + ObjType runtime.Object + NamespaceFilter *NamespaceFilter + AnnotationsFilter *AnnotationsFilter } type UpdateFuncs struct { @@ -90,6 +91,7 @@ func NewGenericReplicator(config ReplicatorConfig) *GenericReplicator { repl.Controller = controller repl.NamespaceFilter = config.NamespaceFilter + repl.AnnotationsFilter = config.AnnotationsFilter return &repl } @@ -429,6 +431,18 @@ func (r *GenericReplicator) getNamespacesToReplicate(myNs string, patterns strin func (r *GenericReplicator) replicateResourceToNamespaces(obj interface{}, targets []v1.Namespace) (replicatedTo []v1.Namespace, err error) { cacheKey := MustGetKey(obj) + metaObj, ok := obj.(metav1.Object) + if !ok { + return nil, fmt.Errorf("object does not implement metav1.Object interface") + } + + if r.AnnotationsFilter != nil && r.AnnotationsFilter.ShouldExclude(metaObj.GetAnnotations()) { + log.WithFields(log.Fields{ + "kind": r.Kind, + }).Info("Skipping object due to excluded annotation") + return + } + for _, namespace := range targets { if r.NamespaceFilter != nil && r.NamespaceFilter.ShouldExclude(namespace.Name) { log.WithFields(log.Fields{ diff --git a/replicate/configmap/configmaps.go b/replicate/configmap/configmaps.go index 33018f20..6dc89001 100644 --- a/replicate/configmap/configmaps.go +++ b/replicate/configmap/configmaps.go @@ -26,7 +26,7 @@ type Replicator struct { } // NewReplicator creates a new config map replicator -func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll, syncByContent bool, namespaceFilter *common.NamespaceFilter) common.Replicator { +func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll, syncByContent bool, namespaceFilter *common.NamespaceFilter, annotationsFilter *common.AnnotationsFilter) common.Replicator { repl := Replicator{ GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{ Kind: "ConfigMap", @@ -41,7 +41,8 @@ func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allo WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { return client.CoreV1().ConfigMaps("").Watch(context.TODO(), lo) }, - NamespaceFilter: namespaceFilter, + NamespaceFilter: namespaceFilter, + AnnotationsFilter: annotationsFilter, }), } repl.UpdateFuncs = common.UpdateFuncs{ diff --git a/replicate/role/roles.go b/replicate/role/roles.go index ec28f9e1..9f958dac 100644 --- a/replicate/role/roles.go +++ b/replicate/role/roles.go @@ -24,7 +24,7 @@ type Replicator struct { } // NewReplicator creates a new role replicator -func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool) common.Replicator { +func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool, annotationsFilter *common.AnnotationsFilter) common.Replicator { repl := Replicator{ GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{ Kind: "Role", @@ -38,6 +38,7 @@ func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allo WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { return client.RbacV1().Roles("").Watch(context.TODO(), lo) }, + AnnotationsFilter: annotationsFilter, }), } repl.UpdateFuncs = common.UpdateFuncs{ diff --git a/replicate/role/roles_test.go b/replicate/role/roles_test.go index 176b530a..6953db8f 100644 --- a/replicate/role/roles_test.go +++ b/replicate/role/roles_test.go @@ -4,8 +4,6 @@ import ( "bytes" "context" "fmt" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/types" "os" "path/filepath" "strings" @@ -13,6 +11,9 @@ import ( "testing" "time" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" + "github.com/mittwald/kubernetes-replicator/replicate/common" pkgerrors "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -78,8 +79,9 @@ func TestRoleReplicator(t *testing.T) { prefix := namespacePrefix() client := kubernetes.NewForConfigOrDie(config) + annotationsFilter := common.NewAnnotationsFilter([]string{}) - repl := NewReplicator(client, 60*time.Second, false) + repl := NewReplicator(client, 60*time.Second, false, annotationsFilter) go repl.Run() time.Sleep(200 * time.Millisecond) diff --git a/replicate/rolebinding/rolebindings.go b/replicate/rolebinding/rolebindings.go index f895863d..c2983bd7 100644 --- a/replicate/rolebinding/rolebindings.go +++ b/replicate/rolebinding/rolebindings.go @@ -26,7 +26,7 @@ type Replicator struct { const sleepTime = 100 * time.Millisecond // NewReplicator creates a new secret replicator -func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool) common.Replicator { +func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool, annotationsFilter *common.AnnotationsFilter) common.Replicator { repl := Replicator{ GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{ Kind: "RoleBinding", @@ -40,6 +40,7 @@ func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allo WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { return client.RbacV1().RoleBindings("").Watch(context.TODO(), lo) }, + AnnotationsFilter: annotationsFilter, }), } repl.UpdateFuncs = common.UpdateFuncs{ @@ -178,7 +179,7 @@ func (r *Replicator) ReplicateObjectTo(sourceObj interface{}, target *v1.Namespa return nil } -//Checks if Role required for RoleBinding exists. Retries a few times before returning error to allow replication to catch up +// Checks if Role required for RoleBinding exists. Retries a few times before returning error to allow replication to catch up func (r *Replicator) canReplicate(targetNameSpace string, roleRef string) (err error) { for i := 0; i < 5; i++ { _, err = r.Client.RbacV1().Roles(targetNameSpace).Get(context.TODO(), roleRef, metav1.GetOptions{}) diff --git a/replicate/secret/secrets.go b/replicate/secret/secrets.go index 192624bb..6f57efdc 100644 --- a/replicate/secret/secrets.go +++ b/replicate/secret/secrets.go @@ -26,7 +26,7 @@ type Replicator struct { } // NewReplicator creates a new secret replicator -func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll, syncByContent bool, namespaceFilter *common.NamespaceFilter) common.Replicator { +func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll, syncByContent bool, namespaceFilter *common.NamespaceFilter, annotationsFilter *common.AnnotationsFilter) common.Replicator { repl := Replicator{ GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{ Kind: "Secret", @@ -41,7 +41,8 @@ func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allo WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { return client.CoreV1().Secrets("").Watch(context.TODO(), lo) }, - NamespaceFilter: namespaceFilter, + NamespaceFilter: namespaceFilter, + AnnotationsFilter: annotationsFilter, }), } repl.UpdateFuncs = common.UpdateFuncs{ diff --git a/replicate/secret/secrets_test.go b/replicate/secret/secrets_test.go index 691d711d..2d40ee29 100644 --- a/replicate/secret/secrets_test.go +++ b/replicate/secret/secrets_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "k8s.io/client-go/tools/clientcmd" "os" "path/filepath" "reflect" @@ -13,6 +12,8 @@ import ( "testing" "time" + "k8s.io/client-go/tools/clientcmd" + "github.com/mittwald/kubernetes-replicator/replicate/common" pkgerrors "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -83,8 +84,9 @@ func TestSecretReplicator(t *testing.T) { client := setupRealClientSet(t) filter := common.NewNamespaceFilter([]string{}) + annotationsFilter := common.NewAnnotationsFilter([]string{}) - repl := NewReplicator(client, 60*time.Second, false, false, filter) + repl := NewReplicator(client, 60*time.Second, false, false, filter, annotationsFilter) go repl.Run() time.Sleep(200 * time.Millisecond) @@ -1295,8 +1297,9 @@ func TestSecretReplicatorSyncByContent(t *testing.T) { ctx := context.TODO() filter := common.NewNamespaceFilter([]string{}) + annotationsFilter := common.NewAnnotationsFilter([]string{}) - repl := NewReplicator(client, 60*time.Second, false, true, filter) + repl := NewReplicator(client, 60*time.Second, false, true, filter, annotationsFilter) go repl.Run() time.Sleep(200 * time.Millisecond) diff --git a/replicate/serviceaccount/serviceaccounts.go b/replicate/serviceaccount/serviceaccounts.go index ebe736de..871dac7f 100644 --- a/replicate/serviceaccount/serviceaccounts.go +++ b/replicate/serviceaccount/serviceaccounts.go @@ -24,7 +24,7 @@ type Replicator struct { } // NewReplicator creates a new serviceaccount replicator -func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool) common.Replicator { +func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool, annotationsFilter *common.AnnotationsFilter) common.Replicator { repl := Replicator{ GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{ Kind: "ServiceAccount", @@ -38,6 +38,7 @@ func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allo WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { return client.CoreV1().ServiceAccounts("").Watch(context.TODO(), lo) }, + AnnotationsFilter: annotationsFilter, }), } repl.UpdateFuncs = common.UpdateFuncs{