Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile.buildx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.24 AS deps
FROM --platform=$BUILDPLATFORM golang:1.25 AS deps
ARG TARGETPLATFORM
ARG BUILDPLATFORM

Expand Down
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ type flags struct {
ReplicateServiceAccounts bool
SyncByContent bool
ExcludeNamespaces string
ExcludeAnnotations string
}
14 changes: 9 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)
}
Expand Down
38 changes: 38 additions & 0 deletions replicate/common/annotations_exclude.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions replicate/common/annotations_exclude_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
32 changes: 23 additions & 9 deletions replicate/common/generic-replicator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -90,6 +91,7 @@ func NewGenericReplicator(config ReplicatorConfig) *GenericReplicator {
repl.Controller = controller

repl.NamespaceFilter = config.NamespaceFilter
repl.AnnotationsFilter = config.AnnotationsFilter

return &repl
}
Expand Down Expand Up @@ -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{
Expand Down
5 changes: 3 additions & 2 deletions replicate/configmap/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand Down
3 changes: 2 additions & 1 deletion replicate/role/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand Down
8 changes: 5 additions & 3 deletions replicate/role/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import (
"bytes"
"context"
"fmt"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/types"
"os"
"path/filepath"
"strings"
"sync"
"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"
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions replicate/rolebinding/rolebindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand Down Expand Up @@ -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{})
Expand Down
5 changes: 3 additions & 2 deletions replicate/secret/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand Down
9 changes: 6 additions & 3 deletions replicate/secret/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"fmt"
"k8s.io/client-go/tools/clientcmd"
"os"
"path/filepath"
"reflect"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion replicate/serviceaccount/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand Down