Skip to content

Commit a5ba2dc

Browse files
authored
Merge pull request #51 from pwittrock/crd-test
Support installing CRDs from yaml files in test package
2 parents e205778 + 430142c commit a5ba2dc

25 files changed

+511
-88
lines changed

pkg/cache/cache_suite_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,33 @@ package cache_test
1818

1919
import (
2020
"testing"
21-
"time"
2221

2322
. "github.com/onsi/ginkgo"
2423
. "github.com/onsi/gomega"
2524
"k8s.io/client-go/kubernetes"
2625
"k8s.io/client-go/rest"
26+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2727
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
28-
"sigs.k8s.io/controller-runtime/pkg/test"
2928
)
3029

3130
func TestSource(t *testing.T) {
3231
RegisterFailHandler(Fail)
33-
RunSpecsWithDefaultAndCustomReporters(t, "Cache Suite", []Reporter{test.NewlineReporter{}})
32+
RunSpecsWithDefaultAndCustomReporters(t, "Cache Suite", []Reporter{envtest.NewlineReporter{}})
3433
}
3534

36-
var testenv *test.Environment
35+
var testenv *envtest.Environment
3736
var cfg *rest.Config
3837
var clientset *kubernetes.Clientset
3938

4039
var _ = BeforeSuite(func(done Done) {
4140
logf.SetLogger(logf.ZapLogger(false))
4241

43-
testenv = &test.Environment{}
42+
testenv = &envtest.Environment{}
4443

4544
var err error
4645
cfg, err = testenv.Start()
4746
Expect(err).NotTo(HaveOccurred())
4847

49-
time.Sleep(1 * time.Second)
50-
5148
clientset, err = kubernetes.NewForConfig(cfg)
5249
Expect(err).NotTo(HaveOccurred())
5350

pkg/client/client_suite_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,34 @@ package client_test
1818

1919
import (
2020
"testing"
21-
"time"
2221

2322
. "github.com/onsi/ginkgo"
2423
. "github.com/onsi/gomega"
2524
"k8s.io/client-go/kubernetes"
2625
"k8s.io/client-go/rest"
27-
"sigs.k8s.io/controller-runtime/pkg/test"
26+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2827

2928
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
3029
)
3130

3231
func TestSource(t *testing.T) {
3332
RegisterFailHandler(Fail)
34-
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{test.NewlineReporter{}})
33+
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
3534
}
3635

37-
var testenv *test.Environment
36+
var testenv *envtest.Environment
3837
var cfg *rest.Config
3938
var clientset *kubernetes.Clientset
4039

4140
var _ = BeforeSuite(func(done Done) {
4241
logf.SetLogger(logf.ZapLogger(false))
4342

44-
testenv = &test.Environment{}
43+
testenv = &envtest.Environment{}
4544

4645
var err error
4746
cfg, err = testenv.Start()
4847
Expect(err).NotTo(HaveOccurred())
4948

50-
time.Sleep(1 * time.Second)
51-
5249
clientset, err = kubernetes.NewForConfig(cfg)
5350
Expect(err).NotTo(HaveOccurred())
5451

pkg/client/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var (
3535
func init() {
3636
// TODO: Fix this to allow double vendoring this library but still register flags on behalf of users
3737
flag.StringVar(&kubeconfig, "kubeconfig", "",
38-
"Path to a kubeconfig. Only required if out-of-cluster.")
38+
"Paths to a kubeconfig. Only required if out-of-cluster.")
3939

4040
flag.StringVar(&masterURL, "master", "",
4141
"The address of the Kubernetes API server. Overrides any value in kubeconfig. "+

pkg/client/fake/client_suite_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ import (
2121

2222
. "github.com/onsi/ginkgo"
2323
. "github.com/onsi/gomega"
24-
"sigs.k8s.io/controller-runtime/pkg/test"
24+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2525

2626
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
2727
)
2828

2929
func TestSource(t *testing.T) {
3030
RegisterFailHandler(Fail)
31-
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{test.NewlineReporter{}})
31+
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
3232
}
3333

3434
var _ = BeforeSuite(func(done Done) {

pkg/controller/controller_suite_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,33 @@ package controller_test
1818

1919
import (
2020
"testing"
21-
"time"
2221

2322
. "github.com/onsi/ginkgo"
2423
. "github.com/onsi/gomega"
2524
"k8s.io/client-go/kubernetes"
2625
"k8s.io/client-go/rest"
26+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2727
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
28-
"sigs.k8s.io/controller-runtime/pkg/test"
2928
)
3029

3130
func TestSource(t *testing.T) {
3231
RegisterFailHandler(Fail)
33-
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{test.NewlineReporter{}})
32+
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
3433
}
3534

36-
var testenv *test.Environment
35+
var testenv *envtest.Environment
3736
var cfg *rest.Config
3837
var clientset *kubernetes.Clientset
3938

4039
var _ = BeforeSuite(func(done Done) {
4140
logf.SetLogger(logf.ZapLogger(false))
4241

43-
testenv = &test.Environment{}
42+
testenv = &envtest.Environment{}
4443

4544
var err error
4645
cfg, err = testenv.Start()
4746
Expect(err).NotTo(HaveOccurred())
4847

49-
time.Sleep(1 * time.Second)
50-
5148
clientset, err = kubernetes.NewForConfig(cfg)
5249
Expect(err).NotTo(HaveOccurred())
5350

pkg/envtest/crd.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
Copyright 2018 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+
"io/ioutil"
21+
"os"
22+
"path/filepath"
23+
"time"
24+
25+
"github.com/ghodss/yaml"
26+
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
27+
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"k8s.io/apimachinery/pkg/util/sets"
30+
"k8s.io/apimachinery/pkg/util/wait"
31+
"k8s.io/client-go/rest"
32+
)
33+
34+
// CRDInstallOptions are the options for installing CRDs
35+
type CRDInstallOptions struct {
36+
// Paths is the path to the directory containing CRDs
37+
Paths []string
38+
39+
// CRDs is a list of CRDs to install
40+
CRDs []*apiextensionsv1beta1.CustomResourceDefinition
41+
42+
// maxTime is the max time to wait
43+
maxTime time.Duration
44+
45+
// pollInterval is the interval to check
46+
pollInterval time.Duration
47+
}
48+
49+
const defaultPollInterval = 100 * time.Millisecond
50+
const defaultMaxWait = 10 * time.Second
51+
52+
// InstallCRDs installs a collection of CRDs into a cluster by reading the crd yaml files from a directory
53+
func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]*apiextensionsv1beta1.CustomResourceDefinition, error) {
54+
defaultCRDOptions(&options)
55+
56+
// Read the CRD yamls into options.CRDs
57+
if err := readCRDFiles(&options); err != nil {
58+
return nil, err
59+
}
60+
61+
// Create the CRDs in the apiserver
62+
if err := CreateCRDs(config, options.CRDs); err != nil {
63+
return options.CRDs, err
64+
}
65+
66+
// Wait for the CRDs to appear as Resources in the apiserver
67+
if err := WaitForCRDs(config, options.CRDs, options); err != nil {
68+
return options.CRDs, err
69+
}
70+
71+
return options.CRDs, nil
72+
}
73+
74+
// readCRDFiles reads the directories of CRDs in options.Paths and adds the CRD structs to options.CRDs
75+
func readCRDFiles(options *CRDInstallOptions) error {
76+
if len(options.Paths) > 0 {
77+
for _, path := range options.Paths {
78+
new, err := readCRDs(path)
79+
if err != nil {
80+
return err
81+
}
82+
options.CRDs = append(options.CRDs, new...)
83+
}
84+
}
85+
return nil
86+
}
87+
88+
// defaultCRDOptions sets the default values for CRDs
89+
func defaultCRDOptions(o *CRDInstallOptions) {
90+
if o.maxTime == 0 {
91+
o.maxTime = defaultMaxWait
92+
}
93+
if o.pollInterval == 0 {
94+
o.pollInterval = defaultPollInterval
95+
}
96+
}
97+
98+
// WaitForCRDs waits for the CRDs to appear in discovery
99+
func WaitForCRDs(config *rest.Config, crds []*apiextensionsv1beta1.CustomResourceDefinition, options CRDInstallOptions) error {
100+
// Add each CRD to a map of GroupVersion to Resource
101+
waitingFor := map[schema.GroupVersion]*sets.String{}
102+
for _, crd := range crds {
103+
gv := schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}
104+
if _, found := waitingFor[gv]; !found {
105+
// Initialize the set
106+
waitingFor[gv] = &sets.String{}
107+
}
108+
// Add the Resource
109+
waitingFor[gv].Insert(crd.Spec.Names.Plural)
110+
}
111+
112+
// Poll until all resources are found in discovery
113+
p := &poller{config: config, waitingFor: waitingFor}
114+
return wait.PollImmediate(options.pollInterval, options.maxTime, p.poll)
115+
}
116+
117+
// poller checks if all the resources have been found in discovery, and returns false if not
118+
type poller struct {
119+
// config is used to get discovery
120+
config *rest.Config
121+
122+
// waitingFor is the map of resources keyed by group version that have not yet been found in discovery
123+
waitingFor map[schema.GroupVersion]*sets.String
124+
}
125+
126+
// poll checks if all the resources have been found in discovery, and returns false if not
127+
func (p *poller) poll() (done bool, err error) {
128+
// Create a new clientset to avoid any client caching of discovery
129+
cs, err := clientset.NewForConfig(p.config)
130+
if err != nil {
131+
return false, err
132+
}
133+
134+
allFound := true
135+
for gv, resources := range p.waitingFor {
136+
// All resources found, do nothing
137+
if resources.Len() == 0 {
138+
delete(p.waitingFor, gv)
139+
continue
140+
}
141+
142+
// Get the Resources for this GroupVersion
143+
// TODO: Maybe the controller-runtime client should be able to do this...
144+
resourceList, err := cs.Discovery().ServerResourcesForGroupVersion(gv.Group + "/" + gv.Version)
145+
if err != nil {
146+
return false, nil
147+
}
148+
149+
// Remove each found resource from the resources set that we are waiting for
150+
for _, resource := range resourceList.APIResources {
151+
resources.Delete(resource.Name)
152+
}
153+
154+
// Still waiting on some resources in this group version
155+
if resources.Len() != 0 {
156+
allFound = false
157+
}
158+
}
159+
return allFound, nil
160+
}
161+
162+
// CreateCRDs creates the CRDs
163+
func CreateCRDs(config *rest.Config, crds []*apiextensionsv1beta1.CustomResourceDefinition) error {
164+
cs, err := clientset.NewForConfig(config)
165+
if err != nil {
166+
return err
167+
}
168+
169+
// Create each CRD
170+
for _, crd := range crds {
171+
if _, err := cs.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd); err != nil {
172+
return err
173+
}
174+
}
175+
return nil
176+
}
177+
178+
// readCRDs reads the CRDs from files and Unmarshals them into structs
179+
func readCRDs(path string) ([]*apiextensionsv1beta1.CustomResourceDefinition, error) {
180+
// Get the CRD files
181+
var files []os.FileInfo
182+
var err error
183+
if files, err = ioutil.ReadDir(path); err != nil {
184+
return nil, err
185+
}
186+
187+
// White list the file extensions that may contain CRDs
188+
crdExts := sets.NewString(".json", ".yaml", ".yml")
189+
190+
var crds []*apiextensionsv1beta1.CustomResourceDefinition
191+
for _, file := range files {
192+
// Only parse whitelisted file types
193+
if !crdExts.Has(filepath.Ext(file.Name())) {
194+
continue
195+
}
196+
197+
// Unmarshal the file into a struct
198+
b, err := ioutil.ReadFile(filepath.Join(path, file.Name()))
199+
if err != nil {
200+
return nil, err
201+
}
202+
crd := &apiextensionsv1beta1.CustomResourceDefinition{}
203+
if err = yaml.Unmarshal(b, crd); err != nil {
204+
return nil, err
205+
}
206+
207+
// Check that it is actually a CRD
208+
if crd.Spec.Names.Kind == "" || crd.Spec.Group == "" {
209+
continue
210+
}
211+
212+
crds = append(crds, crd)
213+
}
214+
return crds, nil
215+
}

pkg/test/doc.go renamed to pkg/envtest/doc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package test provides libaries for integration testing by starting a local control plane
18-
package test
17+
// Package envtest provides libraries for integration testing by starting a local control plane
18+
package envtest

0 commit comments

Comments
 (0)