Skip to content

Commit 6ba031e

Browse files
committed
Re-implement the setup-envtest functionality in a package
There are two main points of re-implementing vs just moving the code: 1. Error handling in the old code base was based on panicking and recovering, where the recover basically just read out a field from the panic value and determined the correct exit code. In a package, we want more nuanced error handling, especially in order to allow test suites to catch the errors and surface them through their own reporting mechanisms. 2. There was a lot of global state in the old code base, "hidden" in the env.Env type that was used as a receiver for all the methods. This re-implementation tries to make the state more explicit, keeping only dependencies (like the remote client and local store) in the environment, while retaining the same behavior as the previous implementation. Tests have been ported over to their respective workflow sub-packages, and a few new ones have been added to cover cases the old test suite for one reason or another did not. Thus, we can be fairly confident that the new implementation does not break old functionality, even if it is a significant rewrite.
1 parent 3cbfa0c commit 6ba031e

Some content is hidden

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

44 files changed

+4589
-31
lines changed

examples/scratch-env/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ require (
4848
golang.org/x/sys v0.21.0 // indirect
4949
golang.org/x/term v0.21.0 // indirect
5050
golang.org/x/text v0.16.0 // indirect
51-
golang.org/x/time v0.3.0 // indirect
51+
golang.org/x/time v0.5.0 // indirect
5252
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
5353
google.golang.org/protobuf v1.34.2 // indirect
5454
gopkg.in/inf.v0 v0.9.1 // indirect

examples/scratch-env/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
141141
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
142142
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
143143
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
144-
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
145-
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
144+
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
145+
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
146146
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
147147
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
148148
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ require (
3030
sigs.k8s.io/yaml v1.4.0
3131
)
3232

33+
require github.com/spf13/afero v1.11.0
34+
3335
require (
3436
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
3537
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
@@ -83,7 +85,7 @@ require (
8385
golang.org/x/sync v0.7.0 // indirect
8486
golang.org/x/term v0.21.0 // indirect
8587
golang.org/x/text v0.16.0 // indirect
86-
golang.org/x/time v0.3.0 // indirect
88+
golang.org/x/time v0.5.0 // indirect
8789
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
8890
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
8991
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
112112
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
113113
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
114114
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
115+
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
116+
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
115117
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
116118
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
117119
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -187,8 +189,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
187189
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
188190
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
189191
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
190-
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
191-
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
192+
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
193+
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
192194
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
193195
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
194196
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

go.work

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
go 1.22.4
2+
3+
use (
4+
.
5+
./examples/scratch-env
6+
./tools/setup-envtest
7+
)

go.work.sum

Lines changed: 403 additions & 0 deletions
Large diffs are not rendered by default.

pkg/envtest/setup/cleanup/cleanup.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cleanup
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/env"
8+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/store"
9+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/versions"
10+
)
11+
12+
// Result is a list of version-platform pairs that were removed from the store.
13+
type Result []store.Item
14+
15+
// Cleanup removes binary packages from disk for all version-platform pairs that match the parameters
16+
//
17+
// Note that both the item collection and the error might be non-nil, if some packages were successfully
18+
// removed (they will be listed in the first return value) and some failed (the errors will be collected
19+
// in the second).
20+
func Cleanup(ctx context.Context, spec versions.Spec, options ...Option) (Result, error) {
21+
cfg := configure(options...)
22+
23+
env, err := env.New(cfg.envOpts...)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
if err := env.Store.Initialize(ctx); err != nil {
29+
return nil, err
30+
}
31+
32+
items, err := env.Store.Remove(ctx, store.Filter{Version: spec, Platform: cfg.platform})
33+
if errors.Is(err, store.ErrUnableToList) {
34+
return nil, err
35+
}
36+
37+
// store.Remove returns an error if _any_ item failed to be removed,
38+
// but it also reports any items that were removed without errors.
39+
// Therefore, both items and err might be non-nil at the same time.
40+
return items, err
41+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package cleanup_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
"github.com/spf13/afero"
10+
11+
"github.com/go-logr/logr"
12+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/cleanup"
13+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/env"
14+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/store"
15+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/testhelpers"
16+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/versions"
17+
)
18+
19+
var (
20+
testLog logr.Logger
21+
ctx context.Context
22+
)
23+
24+
func TestCleanup(t *testing.T) {
25+
testLog = testhelpers.GetLogger()
26+
ctx = logr.NewContext(context.Background(), testLog)
27+
28+
RegisterFailHandler(Fail)
29+
RunSpecs(t, "Cleanup Suite")
30+
}
31+
32+
var _ = Describe("Cleanup", func() {
33+
var (
34+
defaultEnvOpts []env.Option
35+
s *store.Store
36+
)
37+
38+
BeforeEach(func() {
39+
s = testhelpers.NewMockStore()
40+
})
41+
42+
JustBeforeEach(func() {
43+
defaultEnvOpts = []env.Option{
44+
env.WithClient(nil), // ensures we fail if we try to connect
45+
env.WithStore(s),
46+
env.WithFS(afero.NewIOFS(s.Root)),
47+
}
48+
})
49+
50+
Context("when cleanup is run", func() {
51+
version := versions.Spec{
52+
Selector: versions.Concrete{
53+
Major: 1,
54+
Minor: 16,
55+
Patch: 1,
56+
},
57+
}
58+
59+
var (
60+
matching, nonMatching []store.Item
61+
)
62+
63+
BeforeEach(func() {
64+
// ensure there are some versions matching what we're about to delete
65+
var err error
66+
matching, err = s.List(ctx, store.Filter{Version: version, Platform: versions.Platform{OS: "linux", Arch: "amd64"}})
67+
Expect(err).NotTo(HaveOccurred())
68+
Expect(matching).NotTo(BeEmpty(), "found no matching versions before cleanup")
69+
70+
// ensure there are some versions _not_ matching what we're about to delete
71+
nonMatching, err = s.List(ctx, store.Filter{Version: versions.Spec{Selector: versions.PatchSelector{Major: 1, Minor: 17, Patch: versions.AnyPoint}}, Platform: versions.Platform{OS: "linux", Arch: "amd64"}})
72+
Expect(err).NotTo(HaveOccurred())
73+
Expect(nonMatching).NotTo(BeEmpty(), "found no non-matching versions before cleanup")
74+
})
75+
76+
JustBeforeEach(func() {
77+
Expect(cleanup.Cleanup(
78+
ctx,
79+
version,
80+
cleanup.WithPlatform("linux", "amd64"),
81+
cleanup.WithEnvOptions(defaultEnvOpts...),
82+
)).To(Succeed())
83+
})
84+
85+
It("should remove matching versions", func() {
86+
items, err := s.List(ctx, store.Filter{Version: version, Platform: versions.Platform{OS: "linux", Arch: "amd64"}})
87+
Expect(err).NotTo(HaveOccurred())
88+
Expect(items).To(BeEmpty(), "found matching versions after cleanup")
89+
})
90+
91+
It("should not remove non-matching versions", func() {
92+
items, err := s.List(ctx, store.Filter{Version: versions.AnyVersion, Platform: versions.Platform{OS: "*", Arch: "*"}})
93+
Expect(err).NotTo(HaveOccurred())
94+
Expect(items).To(ContainElements(nonMatching), "non-matching items were affected")
95+
})
96+
})
97+
})

pkg/envtest/setup/cleanup/config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cleanup
2+
3+
import (
4+
"runtime"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/env"
7+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/versions"
8+
)
9+
10+
type config struct {
11+
envOpts []env.Option
12+
platform versions.Platform
13+
}
14+
15+
// Option is a functional option for configuring the cleanup process.
16+
type Option func(*config)
17+
18+
// WithEnvOptions adds options to the environment setup.
19+
func WithEnvOptions(opts ...env.Option) Option {
20+
return func(cfg *config) {
21+
cfg.envOpts = append(cfg.envOpts, opts...)
22+
}
23+
}
24+
25+
// WithPlatform sets the platform to use for cleanup.
26+
func WithPlatform(os string, arch string) Option {
27+
return func(cfg *config) {
28+
cfg.platform = versions.Platform{OS: os, Arch: arch}
29+
}
30+
}
31+
32+
func configure(options ...Option) *config {
33+
cfg := &config{
34+
platform: versions.Platform{
35+
Arch: runtime.GOARCH,
36+
OS: runtime.GOOS,
37+
},
38+
}
39+
40+
for _, opt := range options {
41+
opt(cfg)
42+
}
43+
44+
return cfg
45+
}

pkg/envtest/setup/env/assets.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package env
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io/fs"
8+
"path/filepath"
9+
10+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/versions"
11+
"sigs.k8s.io/controller-runtime/pkg/log"
12+
)
13+
14+
var expectedExecutables = []string{
15+
"kube-apiserver",
16+
"etcd",
17+
"kubectl",
18+
}
19+
20+
// TryUseAssetsFromPath attempts to use the assets from the provided path if they match the spec.
21+
// If they do not, or if some executable is missing, it returns an empty string.
22+
func (e *Env) TryUseAssetsFromPath(ctx context.Context, spec versions.Spec, path string) (versions.Spec, bool) {
23+
v, err := versions.FromPath(path)
24+
if err != nil {
25+
ok, checkErr := e.hasAllExecutables(path)
26+
log.FromContext(ctx).Info("has all executables", "ok", ok, "err", checkErr)
27+
if checkErr != nil {
28+
log.FromContext(ctx).Error(errors.Join(err, checkErr), "Failed checking if assets path has all binaries, ignoring", "path", path)
29+
return versions.Spec{}, false
30+
} else if ok {
31+
// If the path has all executables, we can use it even if we can't parse the version.
32+
// The user explicitly asked for this path, so set the version to a wildcard so that
33+
// it passes checks downstream.
34+
return versions.AnyVersion, true
35+
}
36+
37+
log.FromContext(ctx).Error(errors.Join(err, errors.New("some required binaries missing")), "Unable to use assets from path, ignoring", "path", path)
38+
return versions.Spec{}, false
39+
}
40+
41+
if !spec.Matches(*v) {
42+
log.FromContext(ctx).Error(nil, "Assets path does not match spec, ignoring", "path", path, "spec", spec)
43+
return versions.Spec{}, false
44+
}
45+
46+
if ok, err := e.hasAllExecutables(path); err != nil {
47+
log.FromContext(ctx).Error(err, "Failed checking if assets path has all binaries, ignoring", "path", path)
48+
return versions.Spec{}, false
49+
} else if !ok {
50+
log.FromContext(ctx).Error(nil, "Assets path is missing some executables, ignoring", "path", path)
51+
return versions.Spec{}, false
52+
}
53+
54+
return versions.Spec{Selector: v}, true
55+
}
56+
57+
func (e *Env) hasAllExecutables(path string) (bool, error) {
58+
for _, expected := range expectedExecutables {
59+
_, err := e.FS.Open(filepath.Join(path, expected))
60+
if err != nil {
61+
if errors.Is(err, fs.ErrNotExist) {
62+
return false, nil
63+
}
64+
return false, fmt.Errorf("check for existence of %s binary in %s: %w", expected, path, err)
65+
}
66+
}
67+
68+
return true, nil
69+
}

pkg/envtest/setup/env/env.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package env
2+
3+
import (
4+
"io/fs"
5+
6+
"github.com/go-logr/logr"
7+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/remote"
8+
"sigs.k8s.io/controller-runtime/pkg/envtest/setup/store"
9+
)
10+
11+
// KubebuilderAssetsEnvVar is the environment variable that can be used to override the default local storage location
12+
const KubebuilderAssetsEnvVar = "KUBEBUILDER_ASSETS"
13+
14+
// Env encapsulates the environment dependencies.
15+
type Env struct {
16+
*store.Store
17+
remote.Client
18+
fs.FS
19+
}
20+
21+
// Option is a functional option for configuring an environment
22+
type Option func(*Env)
23+
24+
// WithStoreAt sets the path to the store directory.
25+
func WithStoreAt(dir string) Option {
26+
return func(c *Env) { c.Store = store.NewAt(dir) }
27+
}
28+
29+
// WithStore allows injecting a envured store.
30+
func WithStore(store *store.Store) Option {
31+
return func(c *Env) { c.Store = store }
32+
}
33+
34+
// WithClient allows injecting a envured remote client.
35+
func WithClient(client remote.Client) Option { return func(c *Env) { c.Client = client } }
36+
37+
// WithFS allows injecting a configured fs.FS, e.g. for mocking.
38+
// TODO: fix this so it's actually used!
39+
func WithFS(fs fs.FS) Option {
40+
return func(c *Env) {
41+
c.FS = fs
42+
}
43+
}
44+
45+
// New returns a new environment, configured with the provided options.
46+
//
47+
// If no options are provided, it will be created with a production store.Store and remote.Client
48+
// and an OS file system.
49+
func New(options ...Option) (*Env, error) {
50+
env := &Env{
51+
// this is the minimal configuration that won't panic
52+
Client: &remote.GCSClient{ //nolint:staticcheck
53+
Bucket: remote.DefaultBucket, //nolint:staticcheck
54+
Server: remote.DefaultServer, //nolint:staticcheck
55+
Log: logr.Discard(),
56+
},
57+
}
58+
59+
for _, option := range options {
60+
option(env)
61+
}
62+
63+
if env.Store == nil {
64+
dir, err := store.DefaultStoreDir()
65+
if err != nil {
66+
return nil, err
67+
}
68+
env.Store = store.NewAt(dir)
69+
}
70+
71+
return env, nil
72+
}

0 commit comments

Comments
 (0)