Skip to content

Commit b0ed0a9

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 b0ed0a9

File tree

21 files changed

+1358
-345
lines changed

21 files changed

+1358
-345
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: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ go 1.15
44

55
require (
66
github.com/spf13/pflag v1.0.5
7-
k8s.io/client-go v0.19.2
87
sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000
98
)
109

examples/scratch-env/go.sum

Lines changed: 379 additions & 195 deletions
Large diffs are not rendered by default.

examples/scratch-env/main.go

Lines changed: 24 additions & 36 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,34 +45,45 @@ 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())
73+
return 1
74+
}
75+
defer os.Remove(kubeconfigFile.Name())
8276

77+
{
8378
log := log.WithValues("path", kubeconfigFile.Name())
8479
log.V(1).Info("Writing kubeconfig")
8580

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),
81+
kubeConfig, err := user.KubeConfig()
82+
if err != nil {
83+
log.Error(err, "unable to create kubeconfig")
9284
}
93-
kcCtx := kcapi.NewContext()
94-
kcCtx.Cluster = "scratch-env"
95-
kubeConfig.Contexts["scratch-env"] = kcCtx
96-
kubeConfig.CurrentContext = "scratch-env"
9785

98-
if err := writeKubeConfig(kubeConfig, kubeconfigFile); err != nil {
86+
if _, err := kubeconfigFile.Write(kubeConfig); err != nil {
9987
log.Error(err, "unable to save kubeconfig")
10088
return 1
10189
}

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: 54 additions & 38 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,23 @@ 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 {
221-
if te.ControlPlane.APIServer == nil {
222-
te.ControlPlane.APIServer = &controlplane.APIServer{Args: te.getAPIServerFlags()}
206+
apiServer := te.ControlPlane.GetAPIServer()
207+
if len(apiServer.Args) == 0 {
208+
// pass these through separately from above in case something like
209+
// AddUser defaults APIServer.
210+
//
211+
// TODO(directxman12): if/when we feel like making a bigger
212+
// breaking change here, just make APIServer and Etcd non-pointers
213+
// in ControlPlane.
214+
215+
// NB(directxman12): we still pass these in so that things work if the
216+
// user manually specifies them, but in most cases we expect them to
217+
// be nil so that we use the new .Configure() logic.
218+
apiServer.Args = te.KubeAPIServerFlags
223219
}
224220
if te.ControlPlane.Etcd == nil {
225221
te.ControlPlane.Etcd = &controlplane.Etcd{}
@@ -228,11 +224,11 @@ func (te *Environment) Start() (*rest.Config, error) {
228224
if os.Getenv(envAttachOutput) == "true" {
229225
te.AttachControlPlaneOutput = true
230226
}
231-
if te.ControlPlane.APIServer.Out == nil && te.AttachControlPlaneOutput {
232-
te.ControlPlane.APIServer.Out = os.Stdout
227+
if apiServer.Out == nil && te.AttachControlPlaneOutput {
228+
apiServer.Out = os.Stdout
233229
}
234-
if te.ControlPlane.APIServer.Err == nil && te.AttachControlPlaneOutput {
235-
te.ControlPlane.APIServer.Err = os.Stderr
230+
if apiServer.Err == nil && te.AttachControlPlaneOutput {
231+
apiServer.Err = os.Stderr
236232
}
237233
if te.ControlPlane.Etcd.Out == nil && te.AttachControlPlaneOutput {
238234
te.ControlPlane.Etcd.Out = os.Stdout
@@ -242,15 +238,16 @@ func (te *Environment) Start() (*rest.Config, error) {
242238
}
243239

244240
if os.Getenv(envKubeAPIServerBin) == "" {
245-
te.ControlPlane.APIServer.Path = te.getBinAssetPath("kube-apiserver")
241+
apiServer.Path = te.getBinAssetPath("kube-apiserver")
246242
}
247243
if os.Getenv(envEtcdBin) == "" {
248244
te.ControlPlane.Etcd.Path = te.getBinAssetPath("etcd")
249245
}
250246
if os.Getenv(envKubectlBin) == "" {
251247
// we can't just set the path manually (it's behind a function), so set the environment variable instead
248+
// TODO(directxman12): re-evaluate this post pkg/internal/testing refactor
252249
if err := os.Setenv(envKubectlBin, te.getBinAssetPath("kubectl")); err != nil {
253-
return nil, err
250+
return nil, fmt.Errorf("unable to override kubectl environment path: %w", err)
254251
}
255252
}
256253

@@ -259,21 +256,27 @@ func (te *Environment) Start() (*rest.Config, error) {
259256
}
260257
te.ControlPlane.Etcd.StartTimeout = te.ControlPlaneStartTimeout
261258
te.ControlPlane.Etcd.StopTimeout = te.ControlPlaneStopTimeout
262-
te.ControlPlane.APIServer.StartTimeout = te.ControlPlaneStartTimeout
263-
te.ControlPlane.APIServer.StopTimeout = te.ControlPlaneStopTimeout
259+
apiServer.StartTimeout = te.ControlPlaneStartTimeout
260+
apiServer.StopTimeout = te.ControlPlaneStopTimeout
264261

265-
log.V(1).Info("starting control plane", "api server flags", te.ControlPlane.APIServer.Args)
262+
log.V(1).Info("starting control plane", "api server flags", apiServer.Args)
266263
if err := te.startControlPlane(); err != nil {
267-
return nil, err
264+
return nil, fmt.Errorf("unable to start control plane itself: %w", err)
268265
}
269266

270267
// Create the *rest.Config for creating new clients
271-
te.Config = &rest.Config{
272-
Host: te.ControlPlane.APIURL().Host,
268+
baseConfig := &rest.Config{
273269
// gotta go fast during tests -- we don't really care about overwhelming our test API server
274270
QPS: 1000.0,
275271
Burst: 2000.0,
276272
}
273+
274+
adminInfo := User{Name: "admin", Groups: []string{"system:masters"}}
275+
adminUser, err := te.ControlPlane.AddUser(adminInfo, baseConfig)
276+
if err != nil {
277+
return te.Config, fmt.Errorf("unable to provision admin user: %w", err)
278+
}
279+
te.Config = adminUser.Config()
277280
}
278281

279282
// Set the default scheme if nil.
@@ -294,17 +297,30 @@ func (te *Environment) Start() (*rest.Config, error) {
294297
te.CRDInstallOptions.WebhookOptions = te.WebhookInstallOptions
295298
crds, err := InstallCRDs(te.Config, te.CRDInstallOptions)
296299
if err != nil {
297-
return te.Config, err
300+
return te.Config, fmt.Errorf("unable to install CRDs onto control plane: %w", err)
298301
}
299302
te.CRDs = crds
300303

301304
log.V(1).Info("installing webhooks")
302305
if err := te.WebhookInstallOptions.Install(te.Config); err != nil {
303-
return nil, err
306+
return nil, fmt.Errorf("unable to install webhooks onto control plane: %w", err)
304307
}
305308
return te.Config, nil
306309
}
307310

311+
// AddUser provisions a new user for connecting to this Environment. The user will
312+
// have the specified name & belong to the specified groups.
313+
//
314+
// If you specify a "base" config, the returned REST Config will contain those
315+
// settings as well as any required by the authentication method. You can use
316+
// this to easily specify options like QPS.
317+
//
318+
// This is effectively a convinience alias for ControlPlane.AddUser -- see that
319+
// for more low-level details.
320+
func (te *Environment) AddUser(user User, baseConfig *rest.Config) (*AuthenticatedUser, error) {
321+
return te.ControlPlane.AddUser(user, baseConfig)
322+
}
323+
308324
func (te *Environment) startControlPlane() error {
309325
numTries, maxRetries := 0, 5
310326
var err error

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)