feat: Add recursive merge and debounce

This commit is contained in:
Dries De Peuter 2023-04-11 08:43:47 +02:00
parent 1016a4bb55
commit bd689f1bb8
No known key found for this signature in database
5 changed files with 172 additions and 42 deletions

10
go.mod
View file

@ -11,6 +11,13 @@ require (
)
require (
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
)
require (
github.com/bep/debounce v1.2.1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
@ -22,6 +29,7 @@ require (
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/imdario/mergo v0.3.6 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -29,7 +37,9 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/samber/lo v1.38.1
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.2
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.5.0 // indirect

17
go.sum
View file

@ -33,6 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -124,6 +126,10 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -154,17 +160,26 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -189,6 +204,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View file

@ -22,6 +22,7 @@ import (
"k8s.io/client-go/util/homedir"
klog "k8s.io/klog/v2"
"github.com/bep/debounce"
"k8s.io/client-go/tools/clientcmd"
)
@ -134,6 +135,8 @@ func loop(ctx context.Context, clientset *kubernetes.Clientset) {
os.Exit(1)
}
debounced := debounce.New(500 * time.Millisecond)
for event := range watch.ResultChan() {
svc, ok := event.Object.(*v1.Service)
if !ok {
@ -141,16 +144,19 @@ func loop(ctx context.Context, clientset *kubernetes.Clientset) {
}
klog.Infof("Change detected on %s", svc.GetName())
reg, err := discoverData(clientset, namespace)
if err != nil {
klog.Error(err)
continue
}
debounced(func() {
reg, err := discoverData(clientset, namespace)
if err != nil {
klog.Error(err)
return
}
klog.Info("Writing configmap")
if err := updateConfigMap(ctx, clientset, reg); err != nil {
klog.Error(err)
}
})
klog.Info("Writing configmap")
if err := updateConfigMap(ctx, clientset, reg); err != nil {
klog.Error(err)
}
}
}
@ -164,20 +170,13 @@ func discoverData(clientset *kubernetes.Clientset, ns string) (wkRegistry, error
if err != nil {
return reg, err
}
r := regexp.MustCompile(`^well-known.stenic.io/(.+)$`)
for _, svc := range svcs.Items {
for name, value := range svc.ObjectMeta.Annotations {
// Skip any non
if !r.MatchString(name) {
name = resolveName(name)
if name == "" {
continue
}
m := r.FindStringSubmatch(name)
if len(m) != 2 {
klog.Warningf("failed to resolve name: %s", name)
continue
}
name = m[1]
if _, ok := reg[name]; !ok {
reg[name] = make(wkData, 0)
@ -189,17 +188,24 @@ func discoverData(clientset *kubernetes.Clientset, ns string) (wkRegistry, error
klog.Error(err)
}
for k, v := range d {
if _, ok := reg[name][k]; ok {
klog.Warningf("key \"%s\" already exists", k)
}
reg[name][k] = v
}
reg[name].append(d)
}
}
return reg, nil
}
func resolveName(name string) string {
r := regexp.MustCompile(`^well-known.stenic.io/(.+)$`)
if !r.MatchString(name) {
return ""
}
m := r.FindStringSubmatch(name)
if len(m) != 2 {
return ""
}
return m[1]
}
func updateConfigMap(ctx context.Context, client kubernetes.Interface, reg wkRegistry) error {
cm := &v1.ConfigMap{Data: reg.encode()}
cm.Namespace = namespace
@ -226,21 +232,3 @@ func updateConfigMap(ctx context.Context, client kubernetes.Interface, reg wkReg
klog.Infof("Updated ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName())
return nil
}
type wkData map[string]interface{}
type wkRegistry map[string]wkData
func (reg wkRegistry) encode() map[string]string {
d := make(map[string]string, len(reg))
for name, data := range reg {
file, err := json.MarshalIndent(data, "", " ")
if err != nil {
klog.Error(err)
} else {
d[name+".json"] = string(file)
}
}
return d
}

86
server/model.go Normal file
View file

@ -0,0 +1,86 @@
package main
import (
"encoding/json"
"reflect"
klog "k8s.io/klog/v2"
)
type wkData map[string]interface{}
func (d wkData) append(data map[string]interface{}) {
for k, v := range data {
if _, ok := d[k]; ok {
d[k] = mergeStructs(d[k], v)
} else {
d[k] = v
}
}
}
type wkRegistry map[string]wkData
func (reg wkRegistry) encode() map[string]string {
d := make(map[string]string, len(reg))
for name, data := range reg {
file, err := json.MarshalIndent(data, "", " ")
if err != nil {
klog.Error(err)
} else {
d[name+".json"] = string(file)
}
}
return d
}
func mergeMapsRecursive(x1, x2 interface{}) interface{} {
switch x1 := x1.(type) {
case map[string]interface{}:
x2, ok := x2.(map[string]interface{})
if !ok {
return x1
}
for k, v2 := range x2 {
if v1, ok := x1[k]; ok {
x1[k] = mergeMapsRecursive(v1, v2)
} else {
x1[k] = v2
}
}
case nil:
// merge(nil, map[string]interface{...}) -> map[string]interface{...}
x2, ok := x2.(map[string]interface{})
if ok {
return x2
}
}
return x1
}
func mergeStructs(x1, x2 interface{}) interface{} {
if reflect.TypeOf(x1) != reflect.TypeOf(x2) {
return x1
}
switch x1 := x1.(type) {
case []interface{}:
x1 = append(x1, x2.([]interface{})...)
case string:
x1 = x2.(string)
case map[string]interface{}:
x2 := x2.(map[string]interface{})
for k, v2 := range x2 {
if v1, ok := x1[k]; ok {
x1[k] = mergeStructs(v1, v2)
} else {
x1[k] = v2
}
}
default:
klog.Warningf("unknown type: %T", x1)
}
return x1
}

29
server/model_test.go Normal file
View file

@ -0,0 +1,29 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_wkData_append(t *testing.T) {
tests := []struct {
name string
d wkData
data map[string]interface{}
expected wkData
}{
{"empty", wkData{}, wkData{"a": 1}, wkData{"a": 1}},
{"append", wkData{"a": 1}, wkData{"b": 2}, wkData{"a": 1, "b": 2}},
{"existing", wkData{"a": "a", "b": "b"}, wkData{"a": "aa"}, wkData{"a": "a", "b": "b"}},
{"nested", wkData{"a": map[string]interface{}{"nest": "value"}}, wkData{"b": 2}, wkData{"b": 2, "a": map[string]interface{}{"nest": "value"}}},
{"nestedExist", wkData{"a": map[string]interface{}{"nest": "value"}}, wkData{"a": 2}, wkData{"a": map[string]interface{}{"nest": "value"}}},
{"nestedExistMerge", wkData{"a": map[string]interface{}{"nest": "value"}}, wkData{"a": map[string]interface{}{"nest2": "value2"}}, wkData{"a": map[string]interface{}{"nest": "value", "nest2": "value2"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.d.append(tt.data)
assert.Equal(t, tt.expected, tt.d, "expected: %v, got: %v", tt.expected, tt.d)
})
}
}