Skip to content

Commit ef81885

Browse files
add xset package to simplify set-kind controller development (#74)
* init commit for xset util package * some adjustment for pr comments * Xset some optimization (#77) * update comment * add GetXSetPatch for controller * add availableReplicas updatedReadyReplicas ScheduledReplicas * remove GetXTemplate, add GetXSetPatch GetXObjectFromRevision * const to var * use lifecycle label manager to support dynamic label management (#79) * refactor: fix typos, use kube-api labels (#80) * use lifecycle labels from kube-api * fix lisence --------- Co-authored-by: hexin <[email protected]>
1 parent 11e9cda commit ef81885

28 files changed

+5102
-3
lines changed

controller/expectations/cache_expectation.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func NewxCacheExpectations(reader client.Reader, scheme *runtime.Scheme, clock c
6161
}
6262
}
6363

64+
func (r *CacheExpectations) CreateExpectations(controllerKey string) (*CacheExpectation, error) {
65+
return r.initExpectations(controllerKey)
66+
}
67+
6468
// GetExpectations returns the ControlleeExpectations of the given controller.
6569
func (r *CacheExpectations) GetExpectations(controllerKey string) (*CacheExpectation, bool, error) {
6670
exp, exists, err := r.GetByKey(controllerKey)
@@ -182,6 +186,16 @@ func (e *CacheExpectation) Fulfilled() bool {
182186
return satisfied
183187
}
184188

189+
func (e *CacheExpectation) FulFilledFor(gvk schema.GroupVersionKind, namespace, name string) bool {
190+
key := e.getKey(gvk, namespace, name)
191+
item, ok, err := e.items.GetByKey(key)
192+
if err != nil || !ok {
193+
return true
194+
}
195+
eitem := item.(*CacheExpectationItem)
196+
return eitem.Fulfilled()
197+
}
198+
185199
func (e *CacheExpectation) ExpectCreation(gvk schema.GroupVersionKind, namespace, name string) error {
186200
return e.expect(e.getKey(gvk, namespace, name), e.creationObserved(gvk, namespace, name))
187201
}

controller/merge/utils.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2024-2025 KusionStack Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package merge
18+
19+
import (
20+
"encoding/json"
21+
22+
"k8s.io/apimachinery/pkg/util/strategicpatch"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
)
25+
26+
// ThreeWayMergeToTarget Use three-way merge to get a updated instance.
27+
func ThreeWayMergeToTarget(currentRevisionTarget, updateRevisionTarget, currentTarget, emptyObj client.Object) error {
28+
currentRevisionTargetBytes, err := json.Marshal(currentRevisionTarget)
29+
if err != nil {
30+
return err
31+
}
32+
updateRevisionTargetBytes, err := json.Marshal(updateRevisionTarget)
33+
if err != nil {
34+
return err
35+
}
36+
37+
// 1. find the extra changes based on current revision
38+
patch, err := strategicpatch.CreateTwoWayMergePatch(currentRevisionTargetBytes, updateRevisionTargetBytes, emptyObj)
39+
if err != nil {
40+
return err
41+
}
42+
43+
// 2. apply above changes to current target object
44+
// We don't apply the diff between currentTarget and currentRevisionTarget to updateRevisionTarget,
45+
// because the TargetTemplate changes should have the highest priority.
46+
currentTargetBytes, err := json.Marshal(currentTarget)
47+
if err != nil {
48+
return err
49+
}
50+
if updateRevisionTargetBytes, err = strategicpatch.StrategicMergePatch(currentTargetBytes, patch, emptyObj); err != nil {
51+
return err
52+
}
53+
54+
err = json.Unmarshal(updateRevisionTargetBytes, currentTarget)
55+
return err
56+
}

controller/utils/slow_start.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2024-2025 The KusionStack Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import "sync"
20+
21+
const (
22+
SlowStartInitialBatchSize = 1
23+
)
24+
25+
func intMin(l, r int) int {
26+
if l < r {
27+
return l
28+
}
29+
30+
return r
31+
}
32+
33+
// SlowStartBatch tries to call the provided function a total of 'count' times,
34+
// starting slow to check for errors, then speeding up if calls succeed.
35+
//
36+
// It groups the calls into batches, starting with a group of initialBatchSize.
37+
// Within each batch, it may call the function multiple times concurrently.
38+
//
39+
// If a whole batch succeeds, the next batch may get exponentially larger.
40+
// If there are any failures in a batch, all remaining batches are skipped
41+
// after waiting for the current batch to complete.
42+
//
43+
// It returns the number of successful calls to the function.
44+
func SlowStartBatch(count, initialBatchSize int, shortCircuit bool, fn func(int, error) error) (int, error) {
45+
remaining := count
46+
successes := 0
47+
index := 0
48+
var gotErr error
49+
for batchSize := intMin(remaining, initialBatchSize); batchSize > 0; batchSize = intMin(2*batchSize, remaining) {
50+
errCh := make(chan error, batchSize)
51+
var wg sync.WaitGroup
52+
wg.Add(batchSize)
53+
for i := 0; i < batchSize; i++ {
54+
go func(index int) {
55+
defer wg.Done()
56+
if err := fn(index, gotErr); err != nil {
57+
errCh <- err
58+
}
59+
}(index)
60+
index++
61+
}
62+
wg.Wait()
63+
curSuccesses := batchSize - len(errCh)
64+
successes += curSuccesses
65+
if len(errCh) > 0 {
66+
gotErr = <-errCh
67+
if shortCircuit {
68+
return successes, gotErr
69+
}
70+
}
71+
remaining -= batchSize
72+
}
73+
return successes, gotErr
74+
}

controller/utils/slow_start_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
Copyright 2024-2025 The KusionStack Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import (
20+
"fmt"
21+
"sync"
22+
"testing"
23+
)
24+
25+
const expectedErrMsg = "expected error"
26+
27+
func TestSlowStartBatch(t *testing.T) {
28+
testcases := []struct {
29+
name string
30+
count int
31+
errorIndex *int
32+
shortcut bool
33+
34+
expectedCallCount int
35+
expectedSuccessCount int
36+
}{
37+
{
38+
name: "happy pass",
39+
count: 10,
40+
errorIndex: nil,
41+
shortcut: false,
42+
expectedCallCount: 10,
43+
expectedSuccessCount: 10,
44+
},
45+
{
46+
name: "failed without shortcut",
47+
count: 10,
48+
errorIndex: intPointer(5),
49+
shortcut: false,
50+
expectedCallCount: 10,
51+
expectedSuccessCount: 9,
52+
},
53+
{
54+
name: "failed without shortcut",
55+
count: 10,
56+
errorIndex: intPointer(5),
57+
shortcut: true,
58+
expectedCallCount: 7, // 1 count in batch 1 + 2 counts in batch 2 + 4 counts in batch 3
59+
expectedSuccessCount: 6,
60+
},
61+
}
62+
63+
for _, testcase := range testcases {
64+
callCounter := intPointer(0)
65+
successCount, err := SlowStartBatch(testcase.count, 1, testcase.shortcut, buildFn(callCounter, testcase.errorIndex))
66+
if testcase.errorIndex == nil {
67+
if err != nil {
68+
t.Fatalf("case %s has unexpected err: %s", testcase.name, err)
69+
}
70+
} else {
71+
if err == nil {
72+
t.Fatalf("case %s has no expected err", testcase.name)
73+
} else if err.Error() != expectedErrMsg {
74+
t.Fatalf("case %s has expected err with unexpected message: %s", testcase.name, err)
75+
}
76+
}
77+
78+
if successCount != testcase.expectedSuccessCount {
79+
t.Fatalf("case %s gets unexpected success count: expected %d, got %d", testcase.name, testcase.expectedSuccessCount, successCount)
80+
}
81+
82+
if *callCounter != testcase.expectedCallCount {
83+
t.Fatalf("case %s gets unexpected call count: expected %d, got %d", testcase.name, testcase.expectedCallCount, *callCounter)
84+
}
85+
}
86+
}
87+
88+
func buildFn(callCounter, errIdx *int) func(int, error) error {
89+
lock := sync.Mutex{}
90+
return func(i int, err error) error {
91+
lock.Lock()
92+
defer func() {
93+
*callCounter++
94+
lock.Unlock()
95+
}()
96+
97+
if errIdx != nil && *callCounter == *errIdx {
98+
return fmt.Errorf(expectedErrMsg)
99+
}
100+
101+
return nil
102+
}
103+
}
104+
105+
func intPointer(val int) *int {
106+
return &val
107+
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
k8s.io/klog/v2 v2.100.1
2222
k8s.io/kubectl v0.28.4
2323
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
24-
kusionstack.io/kube-api v0.2.0
24+
kusionstack.io/kube-api v0.6.6
2525
sigs.k8s.io/controller-runtime v0.17.3
2626
)
2727

@@ -31,6 +31,7 @@ require (
3131
github.com/beorn7/perks v1.0.1 // indirect
3232
github.com/cespare/xxhash/v2 v2.2.0 // indirect
3333
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
34+
github.com/docker/distribution v2.8.2+incompatible // indirect
3435
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
3536
github.com/fsnotify/fsnotify v1.7.0 // indirect
3637
github.com/go-logr/zapr v1.2.4 // indirect
@@ -50,6 +51,7 @@ require (
5051
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
5152
github.com/modern-go/reflect2 v1.0.2 // indirect
5253
github.com/nxadm/tail v1.4.8 // indirect
54+
github.com/opencontainers/go-digest v1.0.0 // indirect
5355
github.com/pkg/errors v0.9.1 // indirect
5456
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
5557
github.com/prometheus/client_model v0.5.0 // indirect
@@ -120,6 +122,5 @@ replace (
120122
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.22.2
121123
k8s.io/system-validators => k8s.io/system-validators v1.5.0
122124
k8s.io/utils => k8s.io/utils v0.0.0-20240102154912-e7106e64919e
123-
kusionstack.io/kube-api => kusionstack.io/kube-api v0.2.0
124125
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.10.3
125126
)

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1
100100
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
101101
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
102102
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
103+
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
104+
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
103105
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
104106
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
105107
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
@@ -338,6 +340,7 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
338340
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
339341
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
340342
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
343+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
341344
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
342345
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
343346
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -854,6 +857,8 @@ k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCf
854857
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
855858
kusionstack.io/kube-api v0.2.0 h1:40SHCpm9RdabTUTVjhsHWoX+h7djy4jMYYTcbnJ9SQc=
856859
kusionstack.io/kube-api v0.2.0/go.mod h1:fYwuojoLs71ox8uyvyKNsJU4CtPtptSfJ3PUSt/3Hgg=
860+
kusionstack.io/kube-api v0.6.6 h1:gMLUQL/eectQxkosnlv1m/R2xieY2crETliWRcxBICg=
861+
kusionstack.io/kube-api v0.6.6/go.mod h1:J0+EHiroG/88X904Y9TV9iMRcoEuD5tXMTLMBDSwM+Y=
857862
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
858863
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
859864
sigs.k8s.io/controller-runtime v0.10.3 h1:s5Ttmw/B4AuIbwrXD3sfBkXwnPMMWrqpVj4WRt1dano=

hack/boilerplate.go.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2024 KusionStack Authors.
2+
* Copyright 2024-2025 KusionStack Authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)