Skip to content

Commit 018b323

Browse files
Introduce "sync-by-content" flag, implemented for secrets and configmaps (#352)
* Add initial "sync-by-content" mode - Optionally ignore the "replicated-from-version" annotation and always compare by contents - Implemented for secrets and config maps - Add test for secrets for the new feature - Change secret tests to use fake.Clientset instead of running in whatever happened to be your active KUBECONFIG(!) * Refine the resource version updates in the fake client * Remove WIP fake client usage * Update README and mention new --sync-by-content flag --------- Co-authored-by: Martin Helmich <[email protected]>
1 parent d750403 commit 018b323

File tree

9 files changed

+346
-146
lines changed

9 files changed

+346
-146
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ data: {}
157157
The replicator will then copy the `data` attribute of the referenced object into the annotated object and keep them in
158158
sync.
159159

160+
By default, the replicator adds an annotation `replicator.v1.mittwald.de/replicated-from-version` to the target object.
161+
This annotation contains the resource-version of the source object at the time of replication.
162+
163+
##### Sync by Content
164+
165+
When the target object is re-applied with an empty `data` attribute, the replicator will not automatically perform replication.
166+
The reason is that the target already has the `replicated-from-version` annotation with a matching source resource-version.
167+
For Secrets and ConfigMaps, there is the option to synchronize _based on the content_, ignoring the `replicated-from-version` annotation.
168+
169+
To activate this mode, start the replicator with the `--sync-by-content` flag.
170+
160171
#### Special case: TLS secrets
161172

162173
Secrets of type `kubernetes.io/tls` are treated in a special way and need to have a `data["tls.crt"]` and a

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ type flags struct {
1515
ReplicateRoles bool
1616
ReplicateRoleBindings bool
1717
ReplicateServiceAccounts bool
18+
SyncByContent bool
1819
}

go.mod

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
11
module github.com/mittwald/kubernetes-replicator
22

3-
go 1.22
3+
go 1.22.0
4+
5+
toolchain go1.23.0
46

57
require (
68
github.com/hashicorp/go-multierror v1.1.1
79
github.com/pkg/errors v0.9.1
810
github.com/sirupsen/logrus v1.9.3
911
github.com/stretchr/testify v1.9.0
10-
k8s.io/api v0.29.4
11-
k8s.io/apimachinery v0.29.4
12-
k8s.io/client-go v0.29.4
12+
k8s.io/api v0.31.1
13+
k8s.io/apimachinery v0.31.1
14+
k8s.io/client-go v0.31.1
1315
)
1416

1517
require (
16-
github.com/davecgh/go-spew v1.1.1 // indirect
18+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1719
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
18-
github.com/go-logr/logr v1.3.0 // indirect
20+
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
21+
github.com/go-logr/logr v1.4.2 // indirect
1922
github.com/go-openapi/jsonpointer v0.19.6 // indirect
2023
github.com/go-openapi/jsonreference v0.20.2 // indirect
21-
github.com/go-openapi/swag v0.22.3 // indirect
24+
github.com/go-openapi/swag v0.22.4 // indirect
2225
github.com/gogo/protobuf v1.3.2 // indirect
2326
github.com/golang/protobuf v1.5.4 // indirect
2427
github.com/google/gnostic-models v0.6.8 // indirect
2528
github.com/google/go-cmp v0.6.0 // indirect
2629
github.com/google/gofuzz v1.2.0 // indirect
27-
github.com/google/uuid v1.3.0 // indirect
30+
github.com/google/uuid v1.6.0 // indirect
2831
github.com/hashicorp/errwrap v1.0.0 // indirect
2932
github.com/imdario/mergo v0.3.7 // indirect
3033
github.com/josharian/intern v1.0.0 // indirect
@@ -33,23 +36,24 @@ require (
3336
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3437
github.com/modern-go/reflect2 v1.0.2 // indirect
3538
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
36-
github.com/pmezard/go-difflib v1.0.0 // indirect
39+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3740
github.com/spf13/pflag v1.0.5 // indirect
38-
golang.org/x/net v0.23.0 // indirect
39-
golang.org/x/oauth2 v0.10.0 // indirect
40-
golang.org/x/sys v0.18.0 // indirect
41-
golang.org/x/term v0.18.0 // indirect
42-
golang.org/x/text v0.14.0 // indirect
41+
github.com/x448/float16 v0.8.4 // indirect
42+
golang.org/x/net v0.26.0 // indirect
43+
golang.org/x/oauth2 v0.21.0 // indirect
44+
golang.org/x/sys v0.21.0 // indirect
45+
golang.org/x/term v0.21.0 // indirect
46+
golang.org/x/text v0.16.0 // indirect
4347
golang.org/x/time v0.3.0 // indirect
44-
google.golang.org/appengine v1.6.7 // indirect
45-
google.golang.org/protobuf v1.33.0 // indirect
48+
google.golang.org/protobuf v1.34.2 // indirect
49+
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
4650
gopkg.in/inf.v0 v0.9.1 // indirect
4751
gopkg.in/yaml.v2 v2.4.0 // indirect
4852
gopkg.in/yaml.v3 v3.0.1 // indirect
49-
k8s.io/klog/v2 v2.110.1 // indirect
50-
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
51-
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
53+
k8s.io/klog/v2 v2.130.1 // indirect
54+
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
55+
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
5256
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
5357
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
54-
sigs.k8s.io/yaml v1.3.0 // indirect
58+
sigs.k8s.io/yaml v1.4.0 // indirect
5559
)

go.sum

Lines changed: 54 additions & 50 deletions
Large diffs are not rendered by default.

main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func init() {
3636
flag.BoolVar(&f.ReplicateRoles, "replicate-roles", true, "Enable replication of roles")
3737
flag.BoolVar(&f.ReplicateRoleBindings, "replicate-role-bindings", true, "Enable replication of role bindings")
3838
flag.BoolVar(&f.ReplicateServiceAccounts, "replicate-service-accounts", true, "Enable replication of service accounts")
39+
flag.BoolVar(&f.SyncByContent, "sync-by-content", false, "Always compare the contents of source and target resources and force them to be the same")
3940
flag.Parse()
4041

4142
switch strings.ToUpper(strings.TrimSpace(f.LogLevel)) {
@@ -88,13 +89,13 @@ func main() {
8889
client = kubernetes.NewForConfigOrDie(config)
8990

9091
if f.ReplicateSecrets {
91-
secretRepl := secret.NewReplicator(client, f.ResyncPeriod, f.AllowAll)
92+
secretRepl := secret.NewReplicator(client, f.ResyncPeriod, f.AllowAll, f.SyncByContent)
9293
go secretRepl.Run()
9394
enabledReplicators = append(enabledReplicators, secretRepl)
9495
}
9596

9697
if f.ReplicateConfigMaps {
97-
configMapRepl := configmap.NewReplicator(client, f.ResyncPeriod, f.AllowAll)
98+
configMapRepl := configmap.NewReplicator(client, f.ResyncPeriod, f.AllowAll, f.SyncByContent)
9899
go configMapRepl.Run()
99100
enabledReplicators = append(enabledReplicators, configMapRepl)
100101
}

replicate/common/generic-replicator.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package common
33
import (
44
"context"
55
"fmt"
6-
"k8s.io/apimachinery/pkg/labels"
76
"reflect"
87
"regexp"
98
"strconv"
109
"strings"
1110
"time"
1211

12+
"k8s.io/apimachinery/pkg/labels"
13+
1314
"github.com/hashicorp/go-multierror"
1415
"github.com/pkg/errors"
1516
log "github.com/sirupsen/logrus"
@@ -22,13 +23,14 @@ import (
2223
)
2324

2425
type ReplicatorConfig struct {
25-
Kind string
26-
Client kubernetes.Interface
27-
ResyncPeriod time.Duration
28-
AllowAll bool
29-
ListFunc cache.ListFunc
30-
WatchFunc cache.WatchFunc
31-
ObjType runtime.Object
26+
Kind string
27+
Client kubernetes.Interface
28+
ResyncPeriod time.Duration
29+
AllowAll bool
30+
SyncByContent bool
31+
ListFunc cache.ListFunc
32+
WatchFunc cache.WatchFunc
33+
ObjType runtime.Object
3234
}
3335

3436
type UpdateFuncs struct {
@@ -44,6 +46,7 @@ type GenericReplicator struct {
4446
Controller cache.Controller
4547

4648
DependencyMap map[string]map[string]interface{}
49+
DependentMap map[string]string
4750
UpdateFuncs UpdateFuncs
4851

4952
// ReplicateToList is a set that caches the names of all secrets that have a
@@ -60,6 +63,7 @@ func NewGenericReplicator(config ReplicatorConfig) *GenericReplicator {
6063
repl := GenericReplicator{
6164
ReplicatorConfig: config,
6265
DependencyMap: make(map[string]map[string]interface{}),
66+
DependentMap: make(map[string]string),
6367
ReplicateToList: GenericMap[string, struct{}]{},
6468
ReplicateToMatchingList: GenericMap[string, labels.Selector]{},
6569
}
@@ -257,6 +261,24 @@ func (r *GenericReplicator) ResourceAdded(obj interface{}) {
257261
logger.WithError(err).Error("failed to update cache")
258262
}
259263
}
264+
source, ok := r.DependentMap[sourceKey]
265+
if ok {
266+
logger.Debugf("objectMeta %s has source %s", sourceKey, source)
267+
268+
sourceObject, exists, err := r.Store.GetByKey(source)
269+
if err != nil {
270+
logger.Debugf("could not get source %s %s: %s", r.Kind, source, err)
271+
return
272+
} else if !exists {
273+
logger.Debugf("could not get source %s %s: does not exist", r.Kind, source)
274+
return
275+
}
276+
targetMap := map[string]interface{}{MustGetKey(obj): ""}
277+
if err := r.updateDependents(sourceObject, targetMap); err != nil {
278+
logger.WithError(err).
279+
Errorf("Failed to update cache for %s: %v", MustGetKey(objectMeta), err)
280+
}
281+
}
260282

261283
annotations := objectMeta.GetAnnotations()
262284

@@ -323,6 +345,10 @@ func (r *GenericReplicator) resourceAddedReplicateFrom(sourceLocation string, ta
323345

324346
r.DependencyMap[sourceLocation][cacheKey] = nil
325347

348+
if _, ok := r.DependentMap[cacheKey]; !ok {
349+
r.DependentMap[cacheKey] = sourceLocation
350+
}
351+
326352
sourceObject, exists, err := r.Store.GetByKey(sourceLocation)
327353
if err != nil {
328354
return errors.Wrapf(err, "Could not get source %s: %v", sourceLocation, err)

replicate/configmap/configmaps.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package configmap
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
@@ -25,14 +26,15 @@ type Replicator struct {
2526
}
2627

2728
// NewReplicator creates a new config map replicator
28-
func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool) common.Replicator {
29+
func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll, syncByContent bool) common.Replicator {
2930
repl := Replicator{
3031
GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{
31-
Kind: "ConfigMap",
32-
ObjType: &v1.ConfigMap{},
33-
AllowAll: allowAll,
34-
ResyncPeriod: resyncPeriod,
35-
Client: client,
32+
Kind: "ConfigMap",
33+
ObjType: &v1.ConfigMap{},
34+
AllowAll: allowAll,
35+
SyncByContent: syncByContent,
36+
ResyncPeriod: resyncPeriod,
37+
Client: client,
3638
ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) {
3739
return client.CoreV1().ConfigMaps("").List(context.TODO(), lo)
3840
},
@@ -65,7 +67,7 @@ func (r *Replicator) ReplicateDataFrom(sourceObj interface{}, targetObj interfac
6567
targetVersion, ok := target.Annotations[common.ReplicatedFromVersionAnnotation]
6668
sourceVersion := source.ResourceVersion
6769

68-
if ok && targetVersion == sourceVersion {
70+
if ok && targetVersion == sourceVersion && !r.SyncByContent {
6971
logger.Debugf("target %s is already up-to-date", common.MustGetKey(target))
7072
return nil
7173
}
@@ -78,17 +80,38 @@ func (r *Replicator) ReplicateDataFrom(sourceObj interface{}, targetObj interfac
7880
prevKeys, hasPrevKeys := common.PreviouslyPresentKeys(&targetCopy.ObjectMeta)
7981
replicatedKeys := make([]string, 0)
8082

83+
dataChanged := false
8184
for key, value := range source.Data {
85+
oldValue, ok := targetCopy.Data[key]
86+
if ok {
87+
if strings.Compare(value, oldValue) != 0 {
88+
dataChanged = true
89+
}
90+
} else {
91+
dataChanged = true
92+
}
8293
targetCopy.Data[key] = value
8394

8495
replicatedKeys = append(replicatedKeys, key)
8596
delete(prevKeys, key)
8697
}
8798

8899
if source.BinaryData != nil {
89-
targetCopy.BinaryData = make(map[string][]byte)
100+
if targetCopy.BinaryData == nil {
101+
targetCopy.BinaryData = make(map[string][]byte)
102+
}
90103
for key, value := range source.BinaryData {
91-
targetCopy.BinaryData[key] = value
104+
newValue := make([]byte, len(value))
105+
copy(newValue, value)
106+
oldValue, ok := targetCopy.BinaryData[key]
107+
if ok {
108+
if bytes.Compare(newValue, oldValue) != 0 {
109+
dataChanged = true
110+
}
111+
} else {
112+
dataChanged = true
113+
}
114+
targetCopy.BinaryData[key] = newValue
92115

93116
replicatedKeys = append(replicatedKeys, key)
94117
delete(prevKeys, key)
@@ -100,9 +123,15 @@ func (r *Replicator) ReplicateDataFrom(sourceObj interface{}, targetObj interfac
100123
logger.Debugf("removing previously present key %s: not present in source any more", k)
101124
delete(targetCopy.Data, k)
102125
delete(targetCopy.BinaryData, k)
126+
dataChanged = true
103127
}
104128
}
105129

130+
if !dataChanged {
131+
logger.Debugf("target values of %s are already up-to-date", common.MustGetKey(target))
132+
return nil
133+
}
134+
106135
sort.Strings(replicatedKeys)
107136

108137
logger.Infof("updating config map %s/%s", target.Namespace, target.Name)

replicate/secret/secrets.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package secret
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
@@ -25,14 +26,15 @@ type Replicator struct {
2526
}
2627

2728
// NewReplicator creates a new secret replicator
28-
func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll bool) common.Replicator {
29+
func NewReplicator(client kubernetes.Interface, resyncPeriod time.Duration, allowAll, syncByContent bool) common.Replicator {
2930
repl := Replicator{
3031
GenericReplicator: common.NewGenericReplicator(common.ReplicatorConfig{
31-
Kind: "Secret",
32-
ObjType: &v1.Secret{},
33-
AllowAll: allowAll,
34-
ResyncPeriod: resyncPeriod,
35-
Client: client,
32+
Kind: "Secret",
33+
ObjType: &v1.Secret{},
34+
AllowAll: allowAll,
35+
SyncByContent: syncByContent,
36+
ResyncPeriod: resyncPeriod,
37+
Client: client,
3638
ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) {
3739
return client.CoreV1().Secrets("").List(context.TODO(), lo)
3840
},
@@ -69,7 +71,7 @@ func (r *Replicator) ReplicateDataFrom(sourceObj interface{}, targetObj interfac
6971
targetVersion, ok := target.Annotations[common.ReplicatedFromVersionAnnotation]
7072
sourceVersion := source.ResourceVersion
7173

72-
if ok && targetVersion == sourceVersion {
74+
if ok && targetVersion == sourceVersion && !r.SyncByContent {
7375
logger.Debugf("target %s is already up-to-date", common.MustGetKey(target))
7476
return nil
7577
}
@@ -82,9 +84,18 @@ func (r *Replicator) ReplicateDataFrom(sourceObj interface{}, targetObj interfac
8284
prevKeys, hasPrevKeys := common.PreviouslyPresentKeys(&targetCopy.ObjectMeta)
8385
replicatedKeys := make([]string, 0)
8486

87+
dataChanged := false
8588
for key, value := range source.Data {
8689
newValue := make([]byte, len(value))
8790
copy(newValue, value)
91+
oldValue, ok := targetCopy.Data[key]
92+
if ok {
93+
if bytes.Compare(newValue, oldValue) != 0 {
94+
dataChanged = true
95+
}
96+
} else {
97+
dataChanged = true
98+
}
8899
targetCopy.Data[key] = newValue
89100

90101
replicatedKeys = append(replicatedKeys, key)
@@ -95,9 +106,15 @@ func (r *Replicator) ReplicateDataFrom(sourceObj interface{}, targetObj interfac
95106
for k := range prevKeys {
96107
logger.Debugf("removing previously present key %s: not present in source any more", k)
97108
delete(targetCopy.Data, k)
109+
dataChanged = true
98110
}
99111
}
100112

113+
if !dataChanged {
114+
logger.Debugf("target values of %s are already up-to-date", common.MustGetKey(target))
115+
return nil
116+
}
117+
101118
sort.Strings(replicatedKeys)
102119

103120
logger.Infof("updating target %s", common.MustGetKey(target))

0 commit comments

Comments
 (0)