Skip to content

Commit 4961162

Browse files
authored
Merge pull request #12 from sttts/sttts-envtest
🌱 Add envtest
2 parents de8cad7 + e80da95 commit 4961162

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5257
-45
lines changed

.golangci.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ linters:
3131
- gocritic
3232
- gocyclo
3333
- gofmt
34-
- goimports
3534
- goprintffuncname
3635
- gosimple
3736
- govet

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,5 @@ verify:
106106
./hack/verify-licenses.sh
107107

108108
.PHONY: test
109-
test:
109+
test: $(KCP)
110110
./hack/run-tests.sh

client/client.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2025 The KCP 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 client
18+
19+
import (
20+
"fmt"
21+
"sync"
22+
23+
"github.com/hashicorp/golang-lru/v2"
24+
25+
"k8s.io/client-go/rest"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
28+
"github.com/kcp-dev/logicalcluster/v3"
29+
)
30+
31+
// ClusterClient is a cluster-aware client.
32+
type ClusterClient interface {
33+
// Cluster returns the client for the given cluster.
34+
Cluster(cluster logicalcluster.Path) client.Client
35+
}
36+
37+
// clusterClient is a multi-cluster-aware client.
38+
type clusterClient struct {
39+
baseConfig *rest.Config
40+
opts client.Options
41+
42+
lock sync.RWMutex
43+
cache *lru.Cache[logicalcluster.Path, client.Client]
44+
}
45+
46+
// New creates a new cluster-aware client.
47+
func New(cfg *rest.Config, options client.Options) (ClusterClient, error) {
48+
ca, err := lru.New[logicalcluster.Path, client.Client](100)
49+
if err != nil {
50+
return nil, err
51+
}
52+
return &clusterClient{
53+
opts: options,
54+
baseConfig: cfg,
55+
cache: ca,
56+
}, nil
57+
}
58+
59+
func (c *clusterClient) Cluster(cluster logicalcluster.Path) client.Client {
60+
// quick path
61+
c.lock.RLock()
62+
cli, ok := c.cache.Get(cluster)
63+
c.lock.RUnlock()
64+
if ok {
65+
return cli
66+
}
67+
68+
// slow path
69+
c.lock.Lock()
70+
defer c.lock.Unlock()
71+
if cli, ok := c.cache.Get(cluster); ok {
72+
return cli
73+
}
74+
75+
// cache miss
76+
cfg := rest.CopyConfig(c.baseConfig)
77+
cfg.Host += cluster.RequestPath()
78+
cli, err := client.New(cfg, c.opts)
79+
if err != nil {
80+
panic(fmt.Errorf("failed to create client for cluster %s: %w", cluster, err))
81+
}
82+
c.cache.Add(cluster, cli)
83+
return cli
84+
}

envtest/doc.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2025 The KCP 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 envtest provides a test environment for testing code against a
18+
// kcp control plane.
19+
package envtest

envtest/eventually.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
Copyright 2025 The KCP 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 envtest
18+
19+
import (
20+
"fmt"
21+
"time"
22+
23+
"github.com/stretchr/testify/require"
24+
25+
corev1 "k8s.io/api/core/v1"
26+
"k8s.io/apimachinery/pkg/util/wait"
27+
"k8s.io/utils/ptr"
28+
29+
conditionsv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/apis/conditions/v1alpha1"
30+
"github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/util/conditions"
31+
)
32+
33+
// Eventually asserts that given condition will be met in waitFor time, periodically checking target function
34+
// each tick. In addition to require.Eventually, this function t.Logs the reason string value returned by the condition
35+
// function (eventually after 20% of the wait time) to aid in debugging.
36+
func Eventually(t TestingT, condition func() (success bool, reason string), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) {
37+
t.Helper()
38+
39+
var last string
40+
start := time.Now()
41+
require.Eventually(t, func() bool {
42+
t.Helper()
43+
44+
ok, msg := condition()
45+
if time.Since(start) > waitFor/5 {
46+
if !ok && msg != "" && msg != last {
47+
last = msg
48+
t.Logf("Waiting for condition, but got: %s", msg)
49+
} else if ok && msg != "" && last != "" {
50+
t.Logf("Condition became true: %s", msg)
51+
}
52+
}
53+
return ok
54+
}, waitFor, tick, msgAndArgs...)
55+
}
56+
57+
// EventuallyReady asserts that the object returned by getter() eventually has a ready condition.
58+
func EventuallyReady(t TestingT, getter func() (conditions.Getter, error), msgAndArgs ...interface{}) {
59+
t.Helper()
60+
EventuallyCondition(t, getter, Is(conditionsv1alpha1.ReadyCondition, corev1.ConditionTrue), msgAndArgs...)
61+
}
62+
63+
// ConditionEvaluator is a helper for evaluating conditions.
64+
type ConditionEvaluator struct {
65+
conditionType conditionsv1alpha1.ConditionType
66+
isStatus *corev1.ConditionStatus
67+
isNotStatus *corev1.ConditionStatus
68+
reason *string
69+
}
70+
71+
func (c *ConditionEvaluator) matches(object conditions.Getter) (*conditionsv1alpha1.Condition, string, bool) {
72+
condition := conditions.Get(object, c.conditionType)
73+
if condition == nil {
74+
return nil, c.descriptor(), false
75+
}
76+
if c.isStatus != nil && condition.Status != *c.isStatus {
77+
return condition, c.descriptor(), false
78+
}
79+
if c.isNotStatus != nil && condition.Status == *c.isNotStatus {
80+
return condition, c.descriptor(), false
81+
}
82+
if c.reason != nil && condition.Reason != *c.reason {
83+
return condition, c.descriptor(), false
84+
}
85+
return condition, c.descriptor(), true
86+
}
87+
88+
func (c *ConditionEvaluator) descriptor() string {
89+
var descriptor string
90+
if c.isStatus != nil {
91+
descriptor = fmt.Sprintf("%s to be %s", c.conditionType, *c.isStatus)
92+
}
93+
if c.isNotStatus != nil {
94+
descriptor = fmt.Sprintf("%s not to be %s", c.conditionType, *c.isNotStatus)
95+
}
96+
if c.reason != nil {
97+
descriptor += fmt.Sprintf(" (with reason %s)", *c.reason)
98+
}
99+
return descriptor
100+
}
101+
102+
// Is matches if the given condition type is of the given value.
103+
func Is(conditionType conditionsv1alpha1.ConditionType, s corev1.ConditionStatus) *ConditionEvaluator {
104+
return &ConditionEvaluator{
105+
conditionType: conditionType,
106+
isStatus: ptr.To(s),
107+
}
108+
}
109+
110+
// IsNot matches if the given condition type is not of the given value.
111+
func IsNot(conditionType conditionsv1alpha1.ConditionType, s corev1.ConditionStatus) *ConditionEvaluator {
112+
return &ConditionEvaluator{
113+
conditionType: conditionType,
114+
isNotStatus: ptr.To(s),
115+
}
116+
}
117+
118+
// WithReason matches if the given condition type has the given reason.
119+
func (c *ConditionEvaluator) WithReason(reason string) *ConditionEvaluator {
120+
c.reason = &reason
121+
return c
122+
}
123+
124+
// EventuallyCondition asserts that the object returned by getter() eventually has a condition that matches the evaluator.
125+
func EventuallyCondition(t TestingT, getter func() (conditions.Getter, error), evaluator *ConditionEvaluator, msgAndArgs ...interface{}) {
126+
t.Helper()
127+
Eventually(t, func() (bool, string) {
128+
obj, err := getter()
129+
require.NoError(t, err, "Error fetching object")
130+
condition, descriptor, done := evaluator.matches(obj)
131+
var reason string
132+
if !done {
133+
if condition != nil {
134+
reason = fmt.Sprintf("Not done waiting for object %s: %s: %s", descriptor, condition.Reason, condition.Message)
135+
} else {
136+
reason = fmt.Sprintf("Not done waiting for object %s: no condition present", descriptor)
137+
}
138+
}
139+
return done, reason
140+
}, wait.ForeverTestTimeout, 100*time.Millisecond, msgAndArgs...)
141+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Copyright 2021 The Kubernetes 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 addr_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestAddr(t *testing.T) {
27+
t.Parallel()
28+
RegisterFailHandler(Fail)
29+
RunSpecs(t, "Addr Suite")
30+
}

0 commit comments

Comments
 (0)