From 98c7ed0190f13fd64c59cf31b1d348bf1176ebd5 Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Mon, 27 May 2024 10:10:30 -0400 Subject: [PATCH] add ability to pass a KinD config to fixture Adds ability to pass a path to a KinD configuration CR in order to configure a KinD fixture. Closes Issue #9 Signed-off-by: Jay Pipes --- README.md | 42 ++++++++++++ action.go | 16 ++++- assertions.go | 9 ++- connect.go | 17 +++++ fixtures/kind/kind.go | 39 ++++------- fixtures/kind/kind_test.go | 65 +++++++++++++++++++ .../default-single-control-plane.yaml | 17 +++++ .../kind-config-one-cp-one-worker.yaml | 7 ++ .../one-control-plane-one-worker.yaml | 25 +++++++ 9 files changed, 207 insertions(+), 30 deletions(-) create mode 100644 fixtures/kind/kind_test.go create mode 100644 fixtures/kind/testdata/default-single-control-plane.yaml create mode 100644 fixtures/kind/testdata/kind-config-one-cp-one-worker.yaml create mode 100644 fixtures/kind/testdata/one-control-plane-one-worker.yaml diff --git a/README.md b/README.md index 904bf7d..83edf7c 100644 --- a/README.md +++ b/README.md @@ -775,6 +775,48 @@ tests: - kube.get: pods/nginx ``` +#### Passing a KinD configuration + +You may want to pass a custom KinD configuration resource by using the +`fixtures.kind.WithConfigPath()` modifier: + + +```go +import ( + "github.com/gdt-dev/gdt" + gdtkube "github.com/gdt-dev/kube" + gdtkind "github.com/gdt-dev/kube/fixtures/kind" +) + +func TestExample(t *testing.T) { + s, err := gdt.From("path/to/test.yaml") + if err != nil { + t.Fatalf("failed to load tests: %s", err) + } + + configPath := filepath.Join("testdata", "my-kind-config.yaml") + + ctx := context.Background() + ctx = gdt.RegisterFixture( + ctx, "kind", gdtkind.New(), + gdtkind.WithConfigPath(configPath), + ) + err = s.Run(ctx, t) + if err != nil { + t.Fatalf("failed to run tests: %s", err) + } +} +``` + +In your test file, you would list the "kind" fixture in the `fixtures` list: + +```yaml +name: example-using-kind +fixtures: + - kind +tests: + - kube.get: pods/nginx +``` ## Contributing and acknowledgements `gdt` was inspired by [Gabbi](https://github.com/cdent/gabbi), the excellent diff --git a/action.go b/action.go index 257b5fb..02723e0 100644 --- a/action.go +++ b/action.go @@ -162,7 +162,12 @@ func (a *Action) doList( // We already validated the label selector during parse-time opts.LabelSelector = labels.Set(withlabels).String() } - return c.client.Resource(res).Namespace(ns).List( + if c.resourceNamespaced(res) { + return c.client.Resource(res).Namespace(ns).List( + ctx, opts, + ) + } + return c.client.Resource(res).List( ctx, opts, ) } @@ -176,7 +181,14 @@ func (a *Action) doGet( ns string, name string, ) (*unstructured.Unstructured, error) { - return c.client.Resource(res).Namespace(ns).Get( + if c.resourceNamespaced(res) { + return c.client.Resource(res).Namespace(ns).Get( + ctx, + name, + metav1.GetOptions{}, + ) + } + return c.client.Resource(res).Get( ctx, name, metav1.GetOptions{}, diff --git a/assertions.go b/assertions.go index b1043e5..4e81b09 100644 --- a/assertions.go +++ b/assertions.go @@ -274,15 +274,18 @@ func (a *assertions) errorOK() bool { // that has a 404 ErrStatus.Code in it apierr, ok := a.err.(*apierrors.StatusError) if ok { - if !a.expectsNotFound() { + if a.expectsNotFound() { if http.StatusNotFound != int(apierr.ErrStatus.Code) { msg := fmt.Sprintf("got status code %d", apierr.ErrStatus.Code) a.Fail(ExpectedNotFound(msg)) return false } + // "Swallow" the NotFound error since we expected it. + a.err = nil + } else { + a.Fail(apierr) + return false } - // "Swallow" the NotFound error since we expected it. - a.err = nil } } if exp.Error != "" && a.r != nil { diff --git a/connect.go b/connect.go index f80ace5..2d62d90 100644 --- a/connect.go +++ b/connect.go @@ -156,6 +156,23 @@ func (c *connection) gvrFromGVK( return r.Resource, nil } +// resourceNamespaces returns true if the supplied schema.GroupVersionResource +// is namespaced, false otherwise +func (c *connection) resourceNamespaced(gvr schema.GroupVersionResource) bool { + apiResources, err := c.disco.ServerResourcesForGroupVersion( + gvr.GroupVersion().String(), + ) + if err != nil { + panic("expected to find APIResource for GroupVersion " + gvr.GroupVersion().String()) + } + for _, apiResource := range apiResources.APIResources { + if apiResource.Name == gvr.Resource { + return apiResource.Namespaced + } + } + panic("expected to find APIResource for GroupVersionResource " + gvr.Resource) +} + // connect returns a connection with a discovery client and a Kubernetes // client-go DynamicClient to use in communicating with the Kubernetes API // server configured for this Spec diff --git a/fixtures/kind/kind.go b/fixtures/kind/kind.go index fd33120..03cf1f6 100644 --- a/fixtures/kind/kind.go +++ b/fixtures/kind/kind.go @@ -5,22 +5,16 @@ package kind import ( - "bytes" "strings" gdttypes "github.com/gdt-dev/gdt/types" "github.com/samber/lo" "sigs.k8s.io/kind/pkg/cluster" kindconst "sigs.k8s.io/kind/pkg/cluster/constants" - kubeyaml "sigs.k8s.io/yaml" gdtkube "github.com/gdt-dev/kube" ) -const ( - workdirNamePattern = "gdt-kube.kindfix.*" -) - // KindFixture implements `gdttypes.Fixture` and exposes connection/config // information about a running KinD cluster. type KindFixture struct { @@ -36,6 +30,8 @@ type KindFixture struct { // will use the default KinD context, which is "kind-{cluster_name}" // See https://github.com/kubernetes-sigs/kind/blob/3610f606516ccaa88aa098465d8c13af70937050/pkg/cluster/internal/kubeconfig/internal/kubeconfig/helpers.go#L23-L26 Context string + // ConfigPath is a path to the v1alpha4 KinD configuration CR + ConfigPath string } func (f *KindFixture) Start() { @@ -45,7 +41,11 @@ func (f *KindFixture) Start() { if f.isRunning() { return } - if err := f.provider.Create(f.ClusterName); err != nil { + opts := []cluster.CreateOption{} + if f.ConfigPath != "" { + opts = append(opts, cluster.CreateWithConfigFile(f.ConfigPath)) + } + if err := f.provider.Create(f.ClusterName, opts...); err != nil { panic(err) } } @@ -96,24 +96,6 @@ func (f *KindFixture) State(key string) interface{} { return "" } -// normYAML round trips yaml bytes through sigs.k8s.io/yaml to normalize them -// versus other kubernetes ecosystem yaml output -func normYAML(y []byte) ([]byte, error) { - var unstructured interface{} - if err := kubeyaml.Unmarshal(y, &unstructured); err != nil { - return nil, err - } - encoded, err := kubeyaml.Marshal(&unstructured) - if err != nil { - return nil, err - } - // special case: don't write anything when empty - if bytes.Equal(encoded, []byte("{}\n")) { - return []byte{}, nil - } - return encoded, nil -} - type KindFixtureModifier func(*KindFixture) // WithClusterName modifies the KindFixture's cluster name @@ -130,6 +112,13 @@ func WithContext(name string) KindFixtureModifier { } } +// WithConfigPath configures a path to a KinD configuration CR to use +func WithConfigPath(path string) KindFixtureModifier { + return func(f *KindFixture) { + f.ConfigPath = path + } +} + // New returns a fixture that exposes Kubernetes configuration/context // information about a KinD cluster. If no such KinD cluster exists, one will // be created. If the KinD cluster is created, it is destroyed at the end of diff --git a/fixtures/kind/kind_test.go b/fixtures/kind/kind_test.go new file mode 100644 index 0000000..556c0a0 --- /dev/null +++ b/fixtures/kind/kind_test.go @@ -0,0 +1,65 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package kind_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gdt-dev/gdt" + gdtcontext "github.com/gdt-dev/gdt/context" + kindfix "github.com/gdt-dev/kube/fixtures/kind" + "github.com/stretchr/testify/require" +) + +func TestDefaultSingleControlPlane(t *testing.T) { + skipKind(t) + require := require.New(t) + + fp := filepath.Join("testdata", "default-single-control-plane.yaml") + + s, err := gdt.From(fp) + require.Nil(err) + require.NotNil(s) + + ctx := gdtcontext.New() + ctx = gdtcontext.RegisterFixture(ctx, "kind", kindfix.New()) + + err = s.Run(ctx, t) + require.Nil(err) +} + +func TestOneControlPlaneOneWorker(t *testing.T) { + skipKind(t) + require := require.New(t) + + fp := filepath.Join("testdata", "one-control-plane-one-worker.yaml") + + s, err := gdt.From(fp) + require.Nil(err) + require.NotNil(s) + + kindCfgPath := filepath.Join("testdata", "kind-config-one-cp-one-worker.yaml") + + ctx := gdtcontext.New() + ctx = gdtcontext.RegisterFixture( + ctx, "kind-one-cp-one-worker", + kindfix.New( + kindfix.WithClusterName("kind-one-cp-one-worker"), + kindfix.WithConfigPath(kindCfgPath), + ), + ) + + err = s.Run(ctx, t) + require.Nil(err) +} + +func skipKind(t *testing.T) { + _, found := os.LookupEnv("SKIP_KIND") + if found { + t.Skipf("skipping KinD-requiring test") + } +} diff --git a/fixtures/kind/testdata/default-single-control-plane.yaml b/fixtures/kind/testdata/default-single-control-plane.yaml new file mode 100644 index 0000000..eaaa56c --- /dev/null +++ b/fixtures/kind/testdata/default-single-control-plane.yaml @@ -0,0 +1,17 @@ +name: default-single-control-plane +description: test default KinD cluster has a single control plane node +fixtures: + - kind +tests: + - name: list-all-nodes + kube.get: nodes + assert: + len: 1 + - name: single-control-plane-node + kube: + get: + type: nodes + labels: + node-role.kubernetes.io/control-plane: "" + assert: + len: 1 diff --git a/fixtures/kind/testdata/kind-config-one-cp-one-worker.yaml b/fixtures/kind/testdata/kind-config-one-cp-one-worker.yaml new file mode 100644 index 0000000..b7046fe --- /dev/null +++ b/fixtures/kind/testdata/kind-config-one-cp-one-worker.yaml @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +- role: worker + labels: + role: worker diff --git a/fixtures/kind/testdata/one-control-plane-one-worker.yaml b/fixtures/kind/testdata/one-control-plane-one-worker.yaml new file mode 100644 index 0000000..7cb6872 --- /dev/null +++ b/fixtures/kind/testdata/one-control-plane-one-worker.yaml @@ -0,0 +1,25 @@ +name: one-control-plane-one-worker +description: test default KinD cluster has one control plane node and one worker +fixtures: + - kind-one-cp-one-worker +tests: + - name: list-all-nodes + kube.get: nodes + assert: + len: 2 + - name: one-control-plane-node + kube: + get: + type: nodes + labels: + node-role.kubernetes.io/control-plane: "" + assert: + len: 1 + - name: one-worker-node + kube: + get: + type: nodes + labels: + role: worker + assert: + len: 1