Skip to content

Commit 156d4e2

Browse files
committed
Envtest secure serving by default, 1.20 flags
This introduces secure serving by default, transparently switching configs to connect to secure endpoints. Similarly, the insecure endpoints are now disabled by default -- they must be explicitly disabled if you want to use them (note that this will only work on API servers <=1.19). This also turns on flags around the tokenrequest endpoint which are required to be on in 1.20, and have been available for all the versions that we currently support (back to 1.16). There's a whole new set of control plane APIs for provisioning users, and the old "just get a single account" APIs of the internal package are mostly deprecated. The default `Environment.Config` is *NOT* deprecated however -- this is intended to continue working as normal in order to avoid breaking everyone and to keep things in envtest working easily -- most tests will be fine with an admin user. For users that want RBAC, individual users may be provisioned with the ControlPlane.AddUser method.
1 parent 9cbdc4a commit 156d4e2

File tree

18 files changed

+1247
-320
lines changed

18 files changed

+1247
-320
lines changed

.golangci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ linters:
2424
- deadcode
2525
- errcheck
2626
- varcheck
27-
- goconst
2827
- unparam
2928
- ineffassign
3029
- nakedret
@@ -33,3 +32,5 @@ linters:
3332
- dupl
3433
- goimports
3534
- golint
35+
# disabled:
36+
# - goconst is overly aggressive

examples/scratch-env/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.15
44

55
require (
66
github.com/spf13/pflag v1.0.5
7-
k8s.io/client-go v0.19.2
7+
k8s.io/client-go v0.21.0-beta.1
88
sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000
99
)
1010

examples/scratch-env/go.sum

Lines changed: 355 additions & 185 deletions
Large diffs are not rendered by default.

examples/scratch-env/main.go

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@ package main
22

33
import (
44
goflag "flag"
5-
"fmt"
6-
"io"
75
"io/ioutil"
86
"os"
97

108
flag "github.com/spf13/pflag"
119

12-
"k8s.io/client-go/tools/clientcmd"
13-
kcapi "k8s.io/client-go/tools/clientcmd/api"
1410
ctrl "sigs.k8s.io/controller-runtime"
1511
"sigs.k8s.io/controller-runtime/pkg/envtest"
1612
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -22,25 +18,6 @@ var (
2218
attachControlPlaneOut = flag.Bool("debug-env", false, "attach to test env (apiserver & etcd) output -- just a convinience flag to force KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT=true")
2319
)
2420

25-
func writeKubeConfig(kubeConfig *kcapi.Config, kubeconfigFile *os.File) error {
26-
defer kubeconfigFile.Close()
27-
28-
contents, err := clientcmd.Write(*kubeConfig)
29-
if err != nil {
30-
return fmt.Errorf("unable to serialize kubeconfig file: %w", err)
31-
}
32-
33-
amt, err := kubeconfigFile.Write(contents)
34-
if err != nil {
35-
return fmt.Errorf("unable to write kubeconfig file: %w", err)
36-
}
37-
if amt != len(contents) {
38-
fmt.Errorf("unable to write all of the kubeconfig file: %w", io.ErrShortWrite)
39-
}
40-
41-
return nil
42-
}
43-
4421
// have a separate function so we can return an exit code w/o skipping defers
4522
func runMain() int {
4623
loggerOpts := &zap.Options{
@@ -68,41 +45,50 @@ func runMain() int {
6845
cfg, err := env.Start()
6946
if err != nil {
7047
log.Error(err, "unable to start the test environment")
48+
// shut down the environment in case we started it and failed while
49+
// installing CRDs or provisioning users.
50+
if err := env.Stop(); err != nil {
51+
log.Error(err, "unable to stop the test environment after an error (this might be expected, but just though you should know)")
52+
}
7153
return 1
7254
}
7355

7456
log.Info("apiserver running", "host", cfg.Host)
7557

58+
// NB(directxman12): this group is unfortunately named, but various
59+
// kubernetes versions require us to use it to get "admin" access.
60+
user, err := env.ControlPlane.AddUser(envtest.User{
61+
Name: "envtest-admin",
62+
Groups: []string{"system:masters"},
63+
}, nil)
64+
if err != nil {
65+
log.Error(err, "unable to provision admin user, continuing on without it")
66+
return 1
67+
}
68+
7669
// TODO(directxman12): add support for writing to a new context in an existing file
7770
kubeconfigFile, err := ioutil.TempFile("", "scratch-env-kubeconfig-")
7871
if err != nil {
7972
log.Error(err, "unable to create kubeconfig file, continuing on without it")
80-
} else {
81-
defer os.Remove(kubeconfigFile.Name())
82-
83-
log := log.WithValues("path", kubeconfigFile.Name())
84-
log.V(1).Info("Writing kubeconfig")
85-
86-
// TODO(directxman12): this config isn't quite fully specified, but I
87-
// think it's the best we can do for now -- I don't see any obvious
88-
// "rest.Config --> clientcmdapi.Config" helper
89-
kubeConfig := kcapi.NewConfig()
90-
kubeConfig.Clusters["scratch-env"] = &kcapi.Cluster{
91-
Server: fmt.Sprintf("http://%s", cfg.Host),
92-
}
93-
kcCtx := kcapi.NewContext()
94-
kcCtx.Cluster = "scratch-env"
95-
kubeConfig.Contexts["scratch-env"] = kcCtx
96-
kubeConfig.CurrentContext = "scratch-env"
97-
98-
if err := writeKubeConfig(kubeConfig, kubeconfigFile); err != nil {
99-
log.Error(err, "unable to save kubeconfig")
100-
return 1
101-
}
73+
return 1
74+
}
75+
defer os.Remove(kubeconfigFile.Name())
10276

103-
log.Info("Wrote kubeconfig")
77+
log := log.WithValues("path", kubeconfigFile.Name())
78+
log.V(1).Info("Writing kubeconfig")
79+
80+
kubeConfig, err := user.KubeConfig()
81+
if err != nil {
82+
log.Error(err, "unable to create kubeconfig")
83+
}
84+
85+
if _, err := kubeconfigFile.Write(kubeConfig); err != nil {
86+
log.Error(err, "unable to save kubeconfig")
87+
return 1
10488
}
10589

90+
log.Info("Wrote kubeconfig")
91+
10692
if opts := env.WebhookInstallOptions; opts.LocalServingPort != 0 {
10793
log.Info("webhooks configured for", "host", opts.LocalServingHost, "port", opts.LocalServingPort, "dir", opts.LocalServingCertDir)
10894
}

pkg/cluster/cluster_suite_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,14 @@ var _ = BeforeSuite(func(done Done) {
5252
cfg, err = testenv.Start()
5353
Expect(err).NotTo(HaveOccurred())
5454

55-
clientTransport = &http.Transport{}
56-
cfg.Transport = clientTransport
55+
cfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
56+
// NB(directxman12): we can't set Transport *and* use TLS options,
57+
// so we grab the transport right after it gets created so that we can
58+
// type-assert on it (hopefully)?
59+
// hopefully this doesn't break 🤞
60+
clientTransport = rt.(*http.Transport)
61+
return rt
62+
}
5763

5864
clientset, err = kubernetes.NewForConfig(cfg)
5965
Expect(err).NotTo(HaveOccurred())

pkg/controller/controller_suite_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,14 @@ var _ = BeforeSuite(func(done Done) {
6868
cfg, err = testenv.Start()
6969
Expect(err).NotTo(HaveOccurred())
7070

71-
clientTransport = &http.Transport{}
72-
cfg.Transport = clientTransport
71+
cfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
72+
// NB(directxman12): we can't set Transport *and* use TLS options,
73+
// so we grab the transport right after it gets created so that we can
74+
// type-assert on it (hopefully)?
75+
// hopefully this doesn't break 🤞
76+
clientTransport = rt.(*http.Transport)
77+
return rt
78+
}
7379

7480
clientset, err = kubernetes.NewForConfig(cfg)
7581
Expect(err).NotTo(HaveOccurred())

pkg/envtest/crd.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]client.Objec
9595

9696
// Read the CRD yamls into options.CRDs
9797
if err := readCRDFiles(&options); err != nil {
98-
return nil, err
98+
return nil, fmt.Errorf("unable to read CRD files: %w", err)
9999
}
100100

101101
if err := modifyConversionWebhooks(options.CRDs, options.Scheme, options.WebhookOptions); err != nil {
@@ -104,12 +104,12 @@ func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]client.Objec
104104

105105
// Create the CRDs in the apiserver
106106
if err := CreateCRDs(config, options.CRDs); err != nil {
107-
return options.CRDs, err
107+
return options.CRDs, fmt.Errorf("unable to create CRD instances: %w", err)
108108
}
109109

110110
// Wait for the CRDs to appear as Resources in the apiserver
111111
if err := WaitForCRDs(config, options.CRDs, options); err != nil {
112-
return options.CRDs, err
112+
return options.CRDs, fmt.Errorf("something went wrong waiting for CRDs to appear as API resources: %w", err)
113113
}
114114

115115
return options.CRDs, nil
@@ -281,7 +281,7 @@ func UninstallCRDs(config *rest.Config, options CRDInstallOptions) error {
281281
func CreateCRDs(config *rest.Config, crds []client.Object) error {
282282
cs, err := client.New(config, client.Options{})
283283
if err != nil {
284-
return err
284+
return fmt.Errorf("unable to create client: %w", err)
285285
}
286286

287287
// Create each CRD
@@ -292,10 +292,10 @@ func CreateCRDs(config *rest.Config, crds []client.Object) error {
292292
switch {
293293
case apierrors.IsNotFound(err):
294294
if err := cs.Create(context.TODO(), crd); err != nil {
295-
return err
295+
return fmt.Errorf("unable to create CRD %q: %w", crd.GetName(), err)
296296
}
297297
case err != nil:
298-
return err
298+
return fmt.Errorf("unable to get CRD %q to check if it exists: %w", crd.GetName(), err)
299299
default:
300300
log.V(1).Info("CRD already exists, updating", "crd", crd.GetName())
301301
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {

pkg/envtest/server.go

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ type APIServer = controlplane.APIServer
9191
// Etcd is the re-exported Etcd type from the internal testing package
9292
type Etcd = controlplane.Etcd
9393

94+
// User represents a Kubernetes user to provision for auth purposes.
95+
type User = controlplane.User
96+
97+
// AuthenticatedUser represets a Kubernetes user that's been provisioned.
98+
type AuthenticatedUser = controlplane.AuthenticatedUser
99+
94100
// Environment creates a Kubernetes test environment that will start / stop the Kubernetes control plane and
95101
// install extension APIs
96102
type Environment struct {
@@ -181,27 +187,6 @@ func (te *Environment) Stop() error {
181187
return te.ControlPlane.Stop()
182188
}
183189

184-
// getAPIServerFlags returns flags to be used with the Kubernetes API server.
185-
// it returns empty slice for api server defined defaults to be applied if no args specified
186-
func (te Environment) getAPIServerFlags() []string {
187-
// Set default API server flags if not set.
188-
if len(te.KubeAPIServerFlags) == 0 {
189-
return []string{}
190-
}
191-
// Check KubeAPIServerFlags contains service-cluster-ip-range, if not, set default value to service-cluster-ip-range
192-
containServiceClusterIPRange := false
193-
for _, flag := range te.KubeAPIServerFlags {
194-
if strings.Contains(flag, "service-cluster-ip-range") {
195-
containServiceClusterIPRange = true
196-
break
197-
}
198-
}
199-
if !containServiceClusterIPRange {
200-
te.KubeAPIServerFlags = append(te.KubeAPIServerFlags, "--service-cluster-ip-range=10.0.0.0/24")
201-
}
202-
return te.KubeAPIServerFlags
203-
}
204-
205190
// Start starts a local Kubernetes server and updates te.ApiserverPort with the port it is listening on
206191
func (te *Environment) Start() (*rest.Config, error) {
207192
if te.useExistingCluster() {
@@ -214,12 +199,17 @@ func (te *Environment) Start() (*rest.Config, error) {
214199
var err error
215200
te.Config, err = config.GetConfig()
216201
if err != nil {
217-
return nil, err
202+
return nil, fmt.Errorf("unable to get configuration for existing cluster: %w", err)
218203
}
219204
}
220205
} else {
221206
if te.ControlPlane.APIServer == nil {
222-
te.ControlPlane.APIServer = &controlplane.APIServer{Args: te.getAPIServerFlags()}
207+
te.ControlPlane.APIServer = &controlplane.APIServer{
208+
// NB(directxman12): we still pass these in so that things work if the
209+
// user manually specifies them, but in most cases we expect them to
210+
// be nil so that we use the new .Configure() logic.
211+
Args: te.KubeAPIServerFlags,
212+
}
223213
}
224214
if te.ControlPlane.Etcd == nil {
225215
te.ControlPlane.Etcd = &controlplane.Etcd{}
@@ -249,8 +239,9 @@ func (te *Environment) Start() (*rest.Config, error) {
249239
}
250240
if os.Getenv(envKubectlBin) == "" {
251241
// we can't just set the path manually (it's behind a function), so set the environment variable instead
242+
// TODO(directxman12): re-evaluate this post pkg/internal/testing refactor
252243
if err := os.Setenv(envKubectlBin, te.getBinAssetPath("kubectl")); err != nil {
253-
return nil, err
244+
return nil, fmt.Errorf("unable to override kubectl environment path: %w", err)
254245
}
255246
}
256247

@@ -264,16 +255,22 @@ func (te *Environment) Start() (*rest.Config, error) {
264255

265256
log.V(1).Info("starting control plane", "api server flags", te.ControlPlane.APIServer.Args)
266257
if err := te.startControlPlane(); err != nil {
267-
return nil, err
258+
return nil, fmt.Errorf("unable to start control plane itself: %w", err)
268259
}
269260

270261
// Create the *rest.Config for creating new clients
271-
te.Config = &rest.Config{
272-
Host: te.ControlPlane.APIURL().Host,
262+
baseConfig := &rest.Config{
273263
// gotta go fast during tests -- we don't really care about overwhelming our test API server
274264
QPS: 1000.0,
275265
Burst: 2000.0,
276266
}
267+
268+
adminInfo := User{Name: "admin", Groups: []string{"system:masters"}}
269+
adminUser, err := te.ControlPlane.AddUser(adminInfo, baseConfig)
270+
if err != nil {
271+
return te.Config, fmt.Errorf("unable to provision admin user: %w", err)
272+
}
273+
te.Config = adminUser.Config()
277274
}
278275

279276
// Set the default scheme if nil.
@@ -294,13 +291,13 @@ func (te *Environment) Start() (*rest.Config, error) {
294291
te.CRDInstallOptions.WebhookOptions = te.WebhookInstallOptions
295292
crds, err := InstallCRDs(te.Config, te.CRDInstallOptions)
296293
if err != nil {
297-
return te.Config, err
294+
return te.Config, fmt.Errorf("unable to install CRDs onto control plane: %w", err)
298295
}
299296
te.CRDs = crds
300297

301298
log.V(1).Info("installing webhooks")
302299
if err := te.WebhookInstallOptions.Install(te.Config); err != nil {
303-
return nil, err
300+
return nil, fmt.Errorf("unable to install webhooks onto control plane: %w", err)
304301
}
305302
return te.Config, nil
306303
}

pkg/internal/testing/certs/tinyca.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ func (c *TinyCA) NewServingCert(names ...string) (CertPair, error) {
158158
})
159159
}
160160

161+
// ClientInfo describes some Kubernetes user for the purposes of creating
162+
// client certificates.
163+
type ClientInfo struct {
164+
// Name is the user name (embedded as the cert's CommonName)
165+
Name string
166+
// Groups are the groups to which this user belongs (embedded as the cert's
167+
// Organization)
168+
Groups []string
169+
}
170+
171+
// NewClientCert produces a new CertPair suitable for use with Kubernetes
172+
// client cert auth with an API server validating based on this CA.
173+
func (c *TinyCA) NewClientCert(user ClientInfo) (CertPair, error) {
174+
return c.makeCert(certutil.Config{
175+
CommonName: user.Name,
176+
Organization: user.Groups,
177+
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
178+
})
179+
}
180+
161181
func resolveNames(names []string) ([]string, []net.IP, error) {
162182
dnsNames := []string{}
163183
ips := []net.IP{}

0 commit comments

Comments
 (0)