Skip to content

Commit 848c203

Browse files
committed
Add workspace helpers
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com>
1 parent a0b15b5 commit 848c203

File tree

8 files changed

+464
-13
lines changed

8 files changed

+464
-13
lines changed

client/client.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package client
1818

1919
import (
20+
"fmt"
2021
"sync"
2122

2223
"github.com/hashicorp/golang-lru/v2"
@@ -30,7 +31,7 @@ import (
3031
// ClusterClient is a cluster-aware client.
3132
type ClusterClient interface {
3233
// Cluster returns the client for the given cluster.
33-
Cluster(cluster logicalcluster.Path) (client.Client, error)
34+
Cluster(cluster logicalcluster.Path) client.Client
3435
}
3536

3637
// clusterClient is a multi-cluster-aware client.
@@ -55,29 +56,29 @@ func New(cfg *rest.Config, options client.Options) (ClusterClient, error) {
5556
}, nil
5657
}
5758

58-
func (c *clusterClient) Cluster(cluster logicalcluster.Path) (client.Client, error) {
59+
func (c *clusterClient) Cluster(cluster logicalcluster.Path) client.Client {
5960
// quick path
6061
c.lock.RLock()
6162
cli, ok := c.cache.Get(cluster)
6263
c.lock.RUnlock()
6364
if ok {
64-
return cli, nil
65+
return cli
6566
}
6667

6768
// slow path
6869
c.lock.Lock()
6970
defer c.lock.Unlock()
7071
if cli, ok := c.cache.Get(cluster); ok {
71-
return cli, nil
72+
return cli
7273
}
7374

7475
// cache miss
7576
cfg := rest.CopyConfig(c.baseConfig)
7677
cfg.Host += cluster.RequestPath()
7778
cli, err := client.New(cfg, c.opts)
7879
if err != nil {
79-
return nil, err
80+
panic(fmt.Errorf("failed to create client for cluster %s: %w", cluster, err))
8081
}
8182
c.cache.Add(cluster, cli)
82-
return cli, nil
83+
return cli
8384
}

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+
}

envtest/scheme.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2016 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 envtest
18+
19+
import (
20+
"k8s.io/client-go/kubernetes/scheme"
21+
22+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
23+
corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
24+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
25+
topologyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/topology/v1alpha1"
26+
)
27+
28+
func init() {
29+
if err := apisv1alpha1.AddToScheme(scheme.Scheme); err != nil {
30+
log.Info("WARNING: failed to add apis.kcp.io/v1alpha1 to scheme", "error", err)
31+
}
32+
if err := corev1alpha1.AddToScheme(scheme.Scheme); err != nil {
33+
log.Info("WARNING: failed to add core.kcp.io/v1alpha1 to scheme", "error", err)
34+
}
35+
if err := tenancyv1alpha1.AddToScheme(scheme.Scheme); err != nil {
36+
log.Info("WARNING: failed to add tenancy.kcp.io/v1alpha1 to scheme", "error", err)
37+
}
38+
if err := topologyv1alpha1.AddToScheme(scheme.Scheme); err != nil {
39+
log.Info("WARNING: failed to add topology.kcp.io/v1alpha1 to scheme", "error", err)
40+
}
41+
}

envtest/testing.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
// TestingT is implemented by *testing.T and potentially other test frameworks.
20+
type TestingT interface {
21+
Cleanup(func())
22+
Error(args ...any)
23+
Errorf(format string, args ...any)
24+
FailNow()
25+
Failed() bool
26+
Fatal(args ...any)
27+
Fatalf(format string, args ...any)
28+
Helper()
29+
Log(args ...any)
30+
Logf(format string, args ...any)
31+
Name() string
32+
TempDir() string
33+
}

0 commit comments

Comments
 (0)