Skip to content

Commit e790143

Browse files
authored
Merge pull request #3335 from sttts/sttts-sdk-testing-external
🌱 Make exposed testing package work outside
2 parents 412f141 + 8b11f5e commit e790143

File tree

7 files changed

+139
-71
lines changed

7 files changed

+139
-71
lines changed

sdk/testing/config.go

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

1919
import (
20+
"sync"
21+
2022
kcptestingserver "github.com/kcp-dev/kcp/sdk/testing/server"
2123
)
2224

@@ -32,6 +34,9 @@ var (
3234
kubeconfigPath string
3335
shardKubeconfigPaths map[string]string
3436
}{}
37+
38+
externalSetupOnce sync.Once
39+
externalSetupFn func() (kubeconfigPath string, shardKubeconfigPaths map[string]string)
3540
)
3641

3742
// InitSharedKcpServer initializes a shared kcp server fixture. It must be
@@ -45,7 +50,14 @@ func InitSharedKcpServer(opts ...kcptestingserver.Option) {
4550
// InitExternalServer configures a potentially pre-existing shared external kcp
4651
// server. It must be called before SharedKcpServer is called. The shard
4752
// kubeconfigs are optional, but the kubeconfigPath must be provided.
48-
func InitExternalServer(kubeconfigPath string, shardKubeconfigPaths map[string]string) {
49-
externalConfig.kubeconfigPath = kubeconfigPath
50-
externalConfig.shardKubeconfigPaths = shardKubeconfigPaths
53+
func InitExternalServer(fn func() (kubeconfigPath string, shardKubeconfigPaths map[string]string)) {
54+
externalSetupFn = fn
55+
}
56+
57+
func setupExternal() {
58+
externalSetupOnce.Do(func() {
59+
if externalSetupFn != nil {
60+
externalConfig.kubeconfigPath, externalConfig.shardKubeconfigPaths = externalSetupFn()
61+
}
62+
})
5163
}

sdk/testing/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 testing provides utilities for testing of and against KCP. This code
18+
// to be used outside of the KCP repository is experimental and subject to change.
19+
package testing

sdk/testing/kcp.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func PrivateKcpServer(t *testing.T, options ...kcptestingserver.Option) kcptesti
4141

4242
cfg := &kcptestingserver.Config{Name: serverName}
4343
for _, opt := range options {
44-
cfg = opt(cfg)
44+
opt(cfg)
4545
}
4646

4747
auditPolicyArg := false
@@ -74,18 +74,23 @@ func PrivateKcpServer(t *testing.T, options ...kcptestingserver.Option) kcptesti
7474
func SharedKcpServer(t *testing.T) kcptestingserver.RunningServer {
7575
t.Helper()
7676

77+
setupExternal()
7778
if len(externalConfig.kubeconfigPath) > 0 {
7879
// Use a pre-existing external server
7980

80-
t.Logf("shared kcp server will target configuration %q", externalConfig.kubeconfigPath)
81+
t.Logf("Shared kcp server will target configuration %q", externalConfig.kubeconfigPath)
8182
s, err := kcptestingserver.NewExternalKCPServer(sharedConfig.Name, externalConfig.kubeconfigPath, externalConfig.shardKubeconfigPaths, filepath.Join(kcptestinghelpers.RepositoryDir(), ".kcp"))
8283
require.NoError(t, err, "failed to create persistent server fixture")
8384

8485
ctx, cancel := context.WithCancel(context.Background())
8586
t.Cleanup(cancel)
8687

87-
err = kcptestingserver.WaitForReady(ctx, t, s.RootShardSystemMasterBaseConfig(t), true)
88-
require.NoError(t, err, "error waiting for readiness")
88+
rootCfg := s.RootShardSystemMasterBaseConfig(t)
89+
t.Logf("Waiting for readiness for server at %s", rootCfg.Host)
90+
err = kcptestingserver.WaitForReady(ctx, rootCfg)
91+
require.NoError(t, err, "external server is not ready")
92+
93+
kcptestingserver.MonitorEndpoints(t, rootCfg, "/livez", "/readyz")
8994

9095
return s
9196
}

sdk/testing/server/config.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,43 @@ type Config struct {
3131
}
3232

3333
// Option a function that wish to modify a given kcp configuration.
34-
type Option func(*Config) *Config
34+
type Option func(*Config)
3535

3636
// WithScratchDirectories adds custom scratch directories to a kcp configuration.
3737
func WithScratchDirectories(artifactDir, dataDir string) Option {
38-
return func(cfg *Config) *Config {
38+
return func(cfg *Config) {
3939
cfg.ArtifactDir = artifactDir
4040
cfg.DataDir = dataDir
41-
return cfg
4241
}
4342
}
4443

4544
// WithCustomArguments applies provided arguments to a given kcp configuration.
4645
func WithCustomArguments(args ...string) Option {
47-
return func(cfg *Config) *Config {
46+
return func(cfg *Config) {
4847
cfg.Args = args
49-
return cfg
5048
}
5149
}
5250

53-
// WithClientCADir sets the client CA directory for a given kcp configuration.
54-
func WithClientCADir(clientCADir string) Option {
55-
return func(cfg *Config) *Config {
51+
// WithClientCA sets the client CA directory for a given kcp configuration.
52+
// A client CA will automatically created and the --client-ca configured.
53+
func WithClientCA(clientCADir string) Option {
54+
return func(cfg *Config) {
5655
cfg.ClientCADir = clientCADir
57-
return cfg
56+
}
57+
}
58+
59+
// WithRunInProcess sets the kcp server to run in process. This requires extra
60+
// setup of the RunInProcessFunc variable and will only work inside of the kcp
61+
// repository.
62+
func WithRunInProcess() Option {
63+
return func(cfg *Config) {
64+
cfg.RunInProcess = true
65+
}
66+
}
67+
68+
// WithLogToConsole sets the kcp server to log to console.
69+
func WithLogToConsole() Option {
70+
return func(cfg *Config) {
71+
cfg.LogToConsole = true
5872
}
5973
}

sdk/testing/server/fixture.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636

3737
"github.com/egymgmbh/go-prefix-writer/prefixer"
3838
"github.com/stretchr/testify/require"
39+
"golang.org/x/sync/errgroup"
3940
"sigs.k8s.io/yaml"
4041

4142
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -91,8 +92,9 @@ func NewFixture(t *testing.T, cfgs ...Config) Fixture {
9192
// Launch kcp servers and ensure they are ready before starting the test
9293
start := time.Now()
9394
t.Log("Starting kcp servers...")
94-
wg := sync.WaitGroup{}
95-
wg.Add(len(servers))
95+
ctx, cancel := context.WithCancel(context.Background())
96+
t.Cleanup(cancel)
97+
g, ctx := errgroup.WithContext(ctx)
9698
for i, srv := range servers {
9799
var opts []RunOption
98100
if env.LogToConsoleEnvSet() || cfgs[i].LogToConsole {
@@ -105,17 +107,27 @@ func NewFixture(t *testing.T, cfgs ...Config) Fixture {
105107
require.NoError(t, err)
106108

107109
// Wait for the server to become ready
108-
go func(s *kcpServer, i int) {
109-
defer wg.Done()
110+
g.Go(func() error {
111+
if err := srv.loadCfg(); err != nil {
112+
return err
113+
}
114+
115+
rootCfg := srv.RootShardSystemMasterBaseConfig(t)
116+
t.Logf("Waiting for readiness for server at %s", rootCfg.Host)
117+
if err := WaitForReady(srv.ctx, rootCfg); err != nil {
118+
return err
119+
}
110120

111-
err := s.loadCfg()
112-
require.NoError(t, err, "error loading config")
121+
if !cfgs[i].RunInProcess {
122+
rootCfg := srv.RootShardSystemMasterBaseConfig(t)
123+
MonitorEndpoints(t, rootCfg, "/livez", "/readyz")
124+
}
113125

114-
err = WaitForReady(s.ctx, t, s.RootShardSystemMasterBaseConfig(t), !cfgs[i].RunInProcess)
115-
require.NoError(t, err, "kcp server %s never became ready: %v", s.name, err)
116-
}(srv, i)
126+
return nil
127+
})
117128
}
118-
wg.Wait()
129+
err := g.Wait()
130+
require.NoError(t, err, "failed to start kcp servers")
119131

120132
for _, s := range servers {
121133
scrapeMetricsForServer(t, s)
@@ -250,6 +262,8 @@ func Command(executableName, identity string) []string {
250262
binary := executableName
251263
if binDir := os.Getenv(kcpBinariesDirEnvDir); binDir == "" && inKcp {
252264
binary = filepath.Join(kcptestinghelpers.RepositoryBinDir(), executableName)
265+
} else if binDir != "" {
266+
binary = filepath.Join(binDir, executableName)
253267
}
254268

255269
if env.NoGoRunEnvSet() || !inKcp {
@@ -290,9 +304,6 @@ func (c *kcpServer) Run(opts ...RunOption) error {
290304
})
291305
c.ctx = ctx
292306

293-
commandLine := append(StartKcpCommand("KCP"), c.args...)
294-
c.t.Logf("running: %v", strings.Join(commandLine, " "))
295-
296307
// run kcp start in-process for easier debugging
297308
if runOpts.runInProcess {
298309
if RunInProcessFunc == nil {
@@ -314,6 +325,9 @@ func (c *kcpServer) Run(opts ...RunOption) error {
314325
return nil
315326
}
316327

328+
commandLine := append(StartKcpCommand("KCP"), c.args...)
329+
c.t.Logf("running: %v", strings.Join(commandLine, " "))
330+
317331
// NOTE: do not use exec.CommandContext here. That method issues a SIGKILL when the context is done, and we
318332
// want to issue SIGTERM instead, to give the server a chance to shut down cleanly.
319333
cmd := exec.Command(commandLine[0], commandLine[1:]...)
@@ -354,7 +368,10 @@ func (c *kcpServer) Run(opts ...RunOption) error {
354368

355369
if err := cmd.Start(); err != nil {
356370
cleanup()
357-
return err
371+
if os.Getenv(kcpBinariesDirEnvDir) == "" && commandLine[0] == "kcp" {
372+
c.t.Log("Consider setting KCP_BINARIES_DIR pointing to a directory with a kcp binary.")
373+
}
374+
return fmt.Errorf("failed to start kcp: %w", err)
358375
}
359376

360377
c.t.Cleanup(func() {

sdk/testing/server/ready.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"errors"
2222
"fmt"
2323
"strings"
24-
"sync"
2524
"testing"
2625
"time"
2726

@@ -31,9 +30,8 @@ import (
3130
"k8s.io/client-go/rest"
3231
)
3332

34-
func WaitForReady(ctx context.Context, t *testing.T, cfg *rest.Config, keepMonitoring bool) error {
35-
t.Logf("waiting for readiness for server at %s", cfg.Host)
36-
33+
// WaitForReady waits for /livez and then /readyz to return success.
34+
func WaitForReady(ctx context.Context, cfg *rest.Config) error {
3735
cfg = rest.CopyConfig(cfg)
3836
if cfg.NegotiatedSerializer == nil {
3937
cfg.NegotiatedSerializer = kubernetesscheme.Codecs.WithoutConversion()
@@ -44,45 +42,53 @@ func WaitForReady(ctx context.Context, t *testing.T, cfg *rest.Config, keepMonit
4442
return fmt.Errorf("failed to create unversioned client: %w", err)
4543
}
4644

47-
wg := sync.WaitGroup{}
48-
wg.Add(2)
49-
for _, endpoint := range []string{"/livez", "/readyz"} {
50-
go func(endpoint string) {
51-
defer wg.Done()
52-
waitForEndpoint(ctx, t, client, endpoint)
53-
}(endpoint)
45+
if err := waitForEndpoint(ctx, client, "/livez"); err != nil {
46+
return fmt.Errorf("server at %s didn't become ready: %w", cfg.Host, err)
5447
}
55-
wg.Wait()
56-
t.Logf("server at %s is ready", cfg.Host)
57-
58-
if keepMonitoring {
59-
for _, endpoint := range []string{"/livez", "/readyz"} {
60-
go func(endpoint string) {
61-
monitorEndpoint(ctx, t, client, endpoint)
62-
}(endpoint)
63-
}
48+
if err := waitForEndpoint(ctx, client, "/readyz"); err != nil {
49+
return fmt.Errorf("server at %s didn't become ready: %w", cfg.Host, err)
6450
}
51+
6552
return nil
6653
}
6754

68-
func waitForEndpoint(ctx context.Context, t *testing.T, client *rest.RESTClient, endpoint string) {
55+
func waitForEndpoint(ctx context.Context, client *rest.RESTClient, endpoint string) error {
6956
var lastError error
7057
if err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Minute, true, func(ctx context.Context) (bool, error) {
7158
req := rest.NewRequest(client).RequestURI(endpoint)
72-
_, err := req.Do(ctx).Raw()
73-
if err != nil {
59+
if _, err := req.Do(ctx).Raw(); err != nil {
7460
lastError = fmt.Errorf("error contacting %s: failed components: %v", req.URL(), unreadyComponentsFromError(err))
7561
return false, nil
7662
}
77-
78-
t.Logf("success contacting %s", req.URL())
7963
return true, nil
8064
}); err != nil && lastError != nil {
81-
t.Error(lastError)
65+
return lastError
66+
}
67+
return nil
68+
}
69+
70+
// MonitorEndpoints keeps watching the given endpoints and fails t on error.
71+
func MonitorEndpoints(t *testing.T, client *rest.Config, endpoints ...string) {
72+
ctx, cancel := context.WithCancel(context.Background())
73+
t.Cleanup(cancel)
74+
for _, endpoint := range endpoints {
75+
go func(endpoint string) {
76+
monitorEndpoint(ctx, t, client, endpoint)
77+
}(endpoint)
8278
}
8379
}
8480

85-
func monitorEndpoint(ctx context.Context, t *testing.T, client *rest.RESTClient, endpoint string) {
81+
func monitorEndpoint(ctx context.Context, t *testing.T, cfg *rest.Config, endpoint string) {
82+
cfg = rest.CopyConfig(cfg)
83+
if cfg.NegotiatedSerializer == nil {
84+
cfg.NegotiatedSerializer = kubernetesscheme.Codecs.WithoutConversion()
85+
}
86+
client, err := rest.UnversionedRESTClientFor(cfg)
87+
if err != nil {
88+
t.Errorf("failed to create unversioned client: %v", err)
89+
return
90+
}
91+
8692
// we need a shorter deadline than the server, or else:
8793
// timeout.go:135] post-timeout activity - time-elapsed: 23.784917ms, GET "/livez" result: Header called after Handler finished
8894
if deadline, ok := t.Deadline(); ok {

test/e2e/framework/flags.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"errors"
2121
"flag"
2222
"path/filepath"
23-
"testing"
2423

2524
cliflag "k8s.io/component-base/cli/flag"
2625
"k8s.io/klog/v2"
@@ -30,13 +29,6 @@ import (
3029
kcptestinghelpers "github.com/kcp-dev/kcp/sdk/testing/helpers"
3130
)
3231

33-
func init() {
34-
klog.InitFlags(flag.CommandLine)
35-
if err := flag.Lookup("v").Value.Set("4"); err != nil {
36-
panic(err)
37-
}
38-
}
39-
4032
var testConfig = struct {
4133
kcpKubeconfig string
4234
shardKubeconfigs map[string]string
@@ -57,15 +49,18 @@ func complete() {
5749
}
5850

5951
func init() {
52+
klog.InitFlags(flag.CommandLine)
53+
if err := flag.Lookup("v").Value.Set("4"); err != nil {
54+
panic(err)
55+
}
56+
6057
flag.StringVar(&testConfig.kcpKubeconfig, "kcp-kubeconfig", "", "Path to the kubeconfig for a kcp server.")
6158
flag.Var(cliflag.NewMapStringString(&testConfig.shardKubeconfigs), "shard-kubeconfigs", "Paths to the kubeconfigs for a kcp shard server in the format <shard-name>=<kubeconfig-path>. If unset, kcp-kubeconfig is used.")
6259
flag.BoolVar(&testConfig.useDefaultKCPServer, "use-default-kcp-server", false, "Whether to use server configuration from .kcp/admin.kubeconfig.")
6360
flag.StringVar(&testConfig.suites, "suites", "control-plane", "A comma-delimited list of suites to run.")
6461

65-
// Make testing package call flags.Parse()
66-
testing.Init()
67-
68-
complete()
69-
70-
kcptesting.InitExternalServer(testConfig.kcpKubeconfig, testConfig.shardKubeconfigs)
62+
kcptesting.InitExternalServer(func() (string, map[string]string) {
63+
complete()
64+
return testConfig.kcpKubeconfig, testConfig.shardKubeconfigs
65+
})
7166
}

0 commit comments

Comments
 (0)