Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to pass a KinD config to fixture #10

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
Expand All @@ -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{},
Expand Down
9 changes: 6 additions & 3 deletions assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 14 additions & 25 deletions fixtures/kind/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
65 changes: 65 additions & 0 deletions fixtures/kind/kind_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
17 changes: 17 additions & 0 deletions fixtures/kind/testdata/default-single-control-plane.yaml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions fixtures/kind/testdata/kind-config-one-cp-one-worker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
labels:
role: worker
25 changes: 25 additions & 0 deletions fixtures/kind/testdata/one-control-plane-one-worker.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading