diff --git a/action.go b/action.go index a774278..ac6d80a 100644 --- a/action.go +++ b/action.go @@ -4,6 +4,32 @@ package kube +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + "testing" + + "github.com/gdt-dev/gdt/debug" + gdterrors "github.com/gdt-dev/gdt/errors" + "github.com/gdt-dev/gdt/parse" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const ( + // fieldManagerName is the identifier for the field manager we specify in + // Apply requests. + fieldManagerName = "gdt-kube" +) + // Action describes the the Kubernetes-specific action that is performed by the // test. type Action struct { @@ -38,3 +64,386 @@ type Action struct { // selector that should be used to select that `type` of resource. Get *ResourceIdentifier `yaml:"get,omitempty"` } + +// getCommand returns a string of the command that the action will end up +// performing. +func (a *Action) getCommand() string { + if a.Get != nil { + return "get" + } + if a.Create != "" { + return "create" + } + if a.Delete != nil { + return "delete" + } + if a.Apply != "" { + return "apply" + } + return "unknown" +} + +// Do performs a single kube command, returning any runtime error. +// +// `kubeErr` will be filled with any error received from the Kubernetes client +// call. +// +// `out` will be filled with the contents of the command's output, if any. When +// the command is a Get, `out` will be a `*unstructured.Unstructured`. When the +// command is a List, `out` will be a `*unstructured.UnstructuredList`. +func (a *Action) Do( + ctx context.Context, + t *testing.T, + c *connection, + ns string, + kubeErr *error, + out *interface{}, +) error { + cmd := a.getCommand() + + debug.Println(ctx, t, "kube: %s [ns: %s]", cmd, ns) + var err error + switch cmd { + case "get": + err = a.get(ctx, t, c, ns, out) + case "create": + err = a.create(ctx, t, c, ns, out) + case "delete": + err = a.delete(ctx, t, c, ns) + case "apply": + err = a.apply(ctx, t, c, ns, out) + default: + return fmt.Errorf("unknown command") + } + if err != nil && kubeErr != nil { + *kubeErr = err + } + return nil +} + +// get executes either a List() or a Get() call against the Kubernetes API +// server, returning any error returned from the client call and populating +// `out` with the response value. +func (a *Action) get( + ctx context.Context, + t *testing.T, + c *connection, + ns string, + out *interface{}, +) error { + kind, name := a.Get.KindName() + gvk := schema.GroupVersionKind{ + Kind: kind, + } + res, err := c.gvrFromGVK(gvk) + if err != nil { + return err + } + if name == "" { + list, err := a.doList(ctx, t, c, res, ns) + if err == nil { + *out = list + } + return err + } else { + obj, err := a.doGet(ctx, t, c, res, ns, name) + if err == nil { + *out = obj + } + return err + } +} + +// doList performs the List() call for a supplied resource kind +func (a *Action) doList( + ctx context.Context, + t *testing.T, + c *connection, + res schema.GroupVersionResource, + ns string, +) (*unstructured.UnstructuredList, error) { + opts := metav1.ListOptions{} + withlabels := a.Get.Labels() + if withlabels != nil { + // We already validated the label selector during parse-time + opts.LabelSelector = labels.Set(withlabels).String() + } + return c.client.Resource(res).Namespace(ns).List( + ctx, opts, + ) +} + +// doGet performs the Get() call for a supplied resource kind and name +func (a *Action) doGet( + ctx context.Context, + t *testing.T, + c *connection, + res schema.GroupVersionResource, + ns string, + name string, +) (*unstructured.Unstructured, error) { + return c.client.Resource(res).Namespace(ns).Get( + ctx, + name, + metav1.GetOptions{}, + ) +} + +// create executes a Create() call against the Kubernetes API server and +// evaluates any assertions that have been set for the returned results. +func (a *Action) create( + ctx context.Context, + t *testing.T, + c *connection, + ns string, + out *interface{}, +) error { + var err error + var r io.Reader + if probablyFilePath(a.Create) { + path := a.Create + f, err := os.Open(path) + if err != nil { + // This should never happen because we check during parse time + // whether the file can be opened. + rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) + return rterr + } + defer f.Close() + r = f + } else { + // Consider the string to be YAML/JSON content and marshal that into an + // unstructured.Unstructured that we then pass to Create() + r = strings.NewReader(a.Create) + } + + // This is what we return to the caller via the `out` param. It contains + // all of the created objects. This is NOT an + // `unstructured.UnstructuredList` because we may have created multiple + // objects of different Kinds. + createdObjs := []*unstructured.Unstructured{} + + objs, err := unstructuredFromReader(r) + if err != nil { + rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) + return rterr + } + for _, obj := range objs { + gvk := obj.GetObjectKind().GroupVersionKind() + ons := obj.GetNamespace() + if ons == "" { + ons = ns + } + res, err := c.gvrFromGVK(gvk) + if err != nil { + return err + } + obj, err := c.client.Resource(res).Namespace(ons).Create( + ctx, + obj, + metav1.CreateOptions{}, + ) + if err != nil { + return err + } + createdObjs = append(createdObjs, obj) + } + *out = createdObjs + return nil +} + +// apply executes an Apply() call against the Kubernetes API server and +// evaluates any assertions that have been set for the returned results. +func (a *Action) apply( + ctx context.Context, + t *testing.T, + c *connection, + ns string, + out *interface{}, +) error { + var err error + var r io.Reader + if probablyFilePath(a.Apply) { + path := a.Apply + f, err := os.Open(path) + if err != nil { + // This should never happen because we check during parse time + // whether the file can be opened. + rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) + return rterr + } + defer f.Close() + r = f + } else { + // Consider the string to be YAML/JSON content and marshal that into an + // unstructured.Unstructured that we then pass to Apply() + r = strings.NewReader(a.Apply) + } + + // This is what we return to the caller via the `out` param. It contains + // all of the applied objects. This is NOT an + // `unstructured.UnstructuredList` because we may have applied multiple + // objects of different Kinds. + appliedObjs := []*unstructured.Unstructured{} + + objs, err := unstructuredFromReader(r) + if err != nil { + rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) + return rterr + } + for _, obj := range objs { + gvk := obj.GetObjectKind().GroupVersionKind() + ons := obj.GetNamespace() + if ons == "" { + ons = ns + } + res, err := c.gvrFromGVK(gvk) + if err != nil { + return err + } + obj, err := c.client.Resource(res).Namespace(ns).Apply( + ctx, + // NOTE(jaypipes): Not sure why a separate name argument is + // necessary considering `obj` is of type + // `*unstructured.Unstructured` and therefore has the `GetName()` + // method... + obj.GetName(), + obj, + // TODO(jaypipes): Not sure if this hard-coded options struct is + // always going to work. Maybe add ability to control it? + metav1.ApplyOptions{FieldManager: fieldManagerName, Force: true}, + ) + if err != nil { + return err + } + appliedObjs = append(appliedObjs, obj) + } + *out = appliedObjs + return nil +} + +// delete executes either Delete() call against the Kubernetes API server +// and evaluates any assertions that have been set for the returned results. +func (a *Action) delete( + ctx context.Context, + t *testing.T, + c *connection, + ns string, +) error { + if a.Delete.FilePath() != "" { + path := a.Delete.FilePath() + f, err := os.Open(path) + if err != nil { + // This should never happen because we check during parse time + // whether the file can be opened. + rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) + return rterr + } + defer f.Close() + objs, err := unstructuredFromReader(f) + if err != nil { + rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) + return rterr + } + for _, obj := range objs { + gvk := obj.GetObjectKind().GroupVersionKind() + res, err := c.gvrFromGVK(gvk) + if err != nil { + return err + } + name := obj.GetName() + ons := obj.GetNamespace() + if ons == "" { + ons = ns + } + if err = a.doDelete(ctx, t, c, res, name, ns); err != nil { + return err + } + } + return nil + } + + kind, name := a.Delete.KindName() + gvk := schema.GroupVersionKind{ + Kind: kind, + } + res, err := c.gvrFromGVK(gvk) + if err != nil { + return err + } + if name == "" { + return a.doDeleteCollection(ctx, t, c, res, ns) + } + return a.doDelete(ctx, t, c, res, ns, name) +} + +// doDelete performs the Delete() call on a kind and name +func (a *Action) doDelete( + ctx context.Context, + t *testing.T, + c *connection, + res schema.GroupVersionResource, + ns string, + name string, +) error { + return c.client.Resource(res).Namespace(ns).Delete( + ctx, + name, + metav1.DeleteOptions{}, + ) +} + +// doDeleteCollection performs the DeleteCollection() call for the supplied +// resource kind +func (a *Action) doDeleteCollection( + ctx context.Context, + t *testing.T, + c *connection, + res schema.GroupVersionResource, + ns string, +) error { + listOpts := metav1.ListOptions{} + withlabels := a.Delete.Labels() + if withlabels != nil { + // We already validated the label selector during parse-time + listOpts.LabelSelector = labels.Set(withlabels).String() + } + return c.client.Resource(res).Namespace(ns).DeleteCollection( + ctx, + metav1.DeleteOptions{}, + listOpts, + ) +} + +// unstructuredFromReader attempts to read the supplied io.Reader and unmarshal +// the content into zero or more unstructured.Unstructured objects +func unstructuredFromReader( + r io.Reader, +) ([]*unstructured.Unstructured, error) { + yr := yaml.NewYAMLReader(bufio.NewReader(r)) + + objs := []*unstructured.Unstructured{} + for { + raw, err := yr.Read() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + data := parse.ExpandWithFixedDoubleDollar(string(raw)) + + obj := &unstructured.Unstructured{} + decoder := yaml.NewYAMLOrJSONDecoder( + bytes.NewBuffer([]byte(data)), len(data), + ) + if err = decoder.Decode(obj); err != nil { + return nil, err + } + if obj.GetObjectKind().GroupVersionKind().Kind != "" { + objs = append(objs, obj) + } + } + + return objs, nil +} diff --git a/assertions.go b/assertions.go index bcc4c5a..a9cb881 100644 --- a/assertions.go +++ b/assertions.go @@ -200,10 +200,6 @@ func (m *ConditionMatch) UnmarshalYAML(node *yaml.Node) error { type assertions struct { // failures contains the set of error messages for failed assertions failures []error - // terminal indicates there was a failure in evaluating the assertions that - // should be considered a terminal condition (and therefore the test action - // should not be retried). - terminal bool // exp contains the expected conditions to assert against exp *Expect // err is the error returned by the client or action. This is evaluated @@ -228,15 +224,6 @@ func (a *assertions) Failures() []error { return a.failures } -// Terminal returns a bool indicating the assertions failed in a way that is -// not retryable. -func (a *assertions) Terminal() bool { - if a == nil { - return false - } - return a.terminal -} - // OK checks all the assertions against the supplied arguments and returns true // if all assertions pass. func (a *assertions) OK() bool { @@ -244,7 +231,6 @@ func (a *assertions) OK() bool { if exp == nil { if a.err != nil { a.Fail(gdterrors.UnexpectedError(a.err)) - a.terminal = true return false } return true @@ -279,7 +265,6 @@ func (a *assertions) errorOK() bool { if errors.Is(a.err, ErrResourceUnknown) { if !exp.Unknown { a.Fail(a.err) - a.terminal = true return false } // "Swallow" the Unknown error since we expected it. @@ -303,7 +288,6 @@ func (a *assertions) errorOK() bool { if exp.Error != "" && a.r != nil { if a.err == nil { a.Fail(gdterrors.UnexpectedError(a.err)) - a.terminal = true return false } if !strings.Contains(a.err.Error(), exp.Error) { @@ -313,7 +297,6 @@ func (a *assertions) errorOK() bool { } if a.err != nil { a.Fail(gdterrors.UnexpectedError(a.err)) - a.terminal = true return false } return true diff --git a/eval.go b/eval.go index 12df262..8131c78 100644 --- a/eval.go +++ b/eval.go @@ -5,41 +5,26 @@ package kube import ( - "bufio" - "bytes" "context" - "fmt" - "io" - "os" - "strings" "testing" "time" - backoff "github.com/cenkalti/backoff/v4" - gdtcontext "github.com/gdt-dev/gdt/context" + "github.com/cenkalti/backoff/v4" "github.com/gdt-dev/gdt/debug" gdterrors "github.com/gdt-dev/gdt/errors" - "github.com/gdt-dev/gdt/parse" "github.com/gdt-dev/gdt/result" gdttypes "github.com/gdt-dev/gdt/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/yaml" ) const ( // defaultGetTimeout is used as a retry max time if the spec's Timeout has // not been specified. defaultGetTimeout = time.Second * 5 - // fieldManagerName is the identifier for the field manager we specify in - // Apply requests. - fieldManagerName = "gdt-kube" ) -// Run executes the test described by the Kubernetes test. A new Kubernetes -// client request is made during this call. +// Eval performs an action and evaluates the results of that action, returning +// a Result that informs the Scenario about what failed or succeeded. A new +// Kubernetes client request is made during this call. func (s *Spec) Eval(ctx context.Context, t *testing.T) *result.Result { c, err := s.connect(ctx) if err != nil { @@ -47,51 +32,9 @@ func (s *Spec) Eval(ctx context.Context, t *testing.T) *result.Result { result.WithRuntimeError(ConnectError(err)), ) } - var res *result.Result - t.Run(s.Title(), func(t *testing.T) { - if s.Kube.Get != nil { - res = s.get(ctx, t, c) - } - if s.Kube.Create != "" { - res = s.create(ctx, t, c) - } - if s.Kube.Delete != nil { - res = s.delete(ctx, t, c) - } - if s.Kube.Apply != "" { - res = s.apply(ctx, t, c) - } - for _, failure := range res.Failures() { - if gdtcontext.TimedOut(ctx, failure) { - to := s.Timeout - if to != nil && !to.Expected { - t.Error(gdterrors.TimeoutExceeded(to.After, failure)) - } - } else { - t.Error(failure) - } - } - }) - return res -} -// get executes either a List() or a Get() call against the Kubernetes API -// server and evaluates any assertions that have been set for the returned -// results. -func (s *Spec) get( - ctx context.Context, - t *testing.T, - c *connection, -) *result.Result { - kind, name := s.Kube.Get.KindName() - gvk := schema.GroupVersionKind{ - Kind: kind, - } - res, err := c.gvrFromGVK(gvk) - a := newAssertions(s.Assert, err, nil) - if !a.OK() { - return result.New(result.WithFailures(a.Failures()...)) - } + var a gdttypes.Assertions + ns := s.Namespace() // if the Spec has no timeout, default it to a reasonable value var cancel context.CancelFunc @@ -101,8 +44,8 @@ func (s *Spec) get( defer cancel() } - // retry the Get/List and test the assertions until they succeed, there is - // a terminal failure, or the timeout expires. + // retry the action and test the assertions until they succeed, there is a + // terminal failure, or the timeout expires. bo := backoff.WithContext(backoff.NewExponentialBackOff(), ctx) ticker := backoff.NewTicker(bo) attempts := 0 @@ -111,11 +54,18 @@ func (s *Spec) get( attempts++ after := tick.Sub(start) - if name == "" { - a = s.doList(ctx, t, c, res, s.Namespace()) - } else { - a = s.doGet(ctx, t, c, res, name, s.Namespace()) + var kubeErr error + var out interface{} + err := s.Do(ctx, t, c, ns, &kubeErr, &out) + if err != nil { + if err == gdterrors.ErrTimeoutExceeded { + return result.New(result.WithFailures(gdterrors.ErrTimeoutExceeded)) + } + if err == gdterrors.RuntimeError { + return result.New(result.WithRuntimeError(err)) + } } + a = newAssertions(s.Assert, kubeErr, &out) success := a.OK() debug.Println( ctx, t, "%s (try %d after %s) ok: %v", @@ -134,308 +84,3 @@ func (s *Spec) get( } return result.New(result.WithFailures(a.Failures()...)) } - -// doList performs the List() call and assertion check for a supplied resource -// kind and name -func (s *Spec) doList( - ctx context.Context, - t *testing.T, - c *connection, - res schema.GroupVersionResource, - namespace string, -) gdttypes.Assertions { - opts := metav1.ListOptions{} - withlabels := s.Kube.Get.Labels() - if withlabels != nil { - // We already validated the label selector during parse-time - opts.LabelSelector = labels.Set(withlabels).String() - } - list, err := c.client.Resource(res).Namespace(namespace).List( - ctx, opts, - ) - return newAssertions(s.Assert, err, list) -} - -// doGet performs the Get() call and assertion check for a supplied resource -// kind and name -func (s *Spec) doGet( - ctx context.Context, - t *testing.T, - c *connection, - res schema.GroupVersionResource, - name string, - namespace string, -) gdttypes.Assertions { - obj, err := c.client.Resource(res).Namespace(namespace).Get( - ctx, - name, - metav1.GetOptions{}, - ) - return newAssertions(s.Assert, err, obj) -} - -// splitKindName returns the Kind for a supplied `Get` or `Delete` command -// where the user can specify either a resource kind or alias, e.g. "pods" or -// "po", or the resource kind followed by a forward slash and a resource name. -func splitKindName(subject string) (string, string) { - kind, name, _ := strings.Cut(subject, "/") - return kind, name -} - -// create executes a Create() call against the Kubernetes API server and -// evaluates any assertions that have been set for the returned results. -func (s *Spec) create( - ctx context.Context, - t *testing.T, - c *connection, -) *result.Result { - var err error - var r io.Reader - if probablyFilePath(s.Kube.Create) { - path := s.Kube.Create - f, err := os.Open(path) - if err != nil { - // This should never happen because we check during parse time - // whether the file can be opened. - rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) - return result.New(result.WithRuntimeError(rterr)) - } - defer f.Close() - r = f - } else { - // Consider the string to be YAML/JSON content and marshal that into an - // unstructured.Unstructured that we then pass to Create() - r = strings.NewReader(s.Kube.Create) - } - - objs, err := unstructuredFromReader(r) - if err != nil { - rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) - return result.New(result.WithRuntimeError(rterr)) - } - for _, obj := range objs { - gvk := obj.GetObjectKind().GroupVersionKind() - ns := obj.GetNamespace() - if ns == "" { - ns = s.Namespace() - } - res, err := c.gvrFromGVK(gvk) - a := newAssertions(s.Assert, err, nil) - if !a.OK() { - return result.New(result.WithFailures(a.Failures()...)) - } - obj, err := c.client.Resource(res).Namespace(ns).Create( - ctx, - obj, - metav1.CreateOptions{}, - ) - // TODO(jaypipes): Clearly this is applying the same assertion to each - // object that was created, which is wrong. When I add the polymorphism - // to the Assertions struct, I will modify this block to look for an - // indexed set of error assertions. - a = newAssertions(s.Assert, err, obj) - return result.New(result.WithFailures(a.Failures()...)) - } - return nil -} - -// apply executes an Apply() call against the Kubernetes API server and -// evaluates any assertions that have been set for the returned results. -func (s *Spec) apply( - ctx context.Context, - t *testing.T, - c *connection, -) *result.Result { - var err error - var r io.Reader - if probablyFilePath(s.Kube.Apply) { - path := s.Kube.Apply - f, err := os.Open(path) - if err != nil { - // This should never happen because we check during parse time - // whether the file can be opened. - rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) - return result.New(result.WithRuntimeError(rterr)) - } - defer f.Close() - r = f - } else { - // Consider the string to be YAML/JSON content and marshal that into an - // unstructured.Unstructured that we then pass to Apply() - r = strings.NewReader(s.Kube.Apply) - } - - objs, err := unstructuredFromReader(r) - if err != nil { - rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) - return result.New(result.WithRuntimeError(rterr)) - } - for _, obj := range objs { - gvk := obj.GetObjectKind().GroupVersionKind() - ns := obj.GetNamespace() - if ns == "" { - ns = s.Namespace() - } - res, err := c.gvrFromGVK(gvk) - a := newAssertions(s.Assert, err, nil) - if !a.OK() { - return result.New(result.WithFailures(a.Failures()...)) - } - obj, err := c.client.Resource(res).Namespace(ns).Apply( - ctx, - // NOTE(jaypipes): Not sure why a separate name argument is - // necessary considering `obj` is of type - // `*unstructured.Unstructured` and therefore has the `GetName()` - // method... - obj.GetName(), - obj, - // TODO(jaypipes): Not sure if this hard-coded options struct is - // always going to work. Maybe add ability to control it? - metav1.ApplyOptions{FieldManager: fieldManagerName, Force: true}, - ) - // TODO(jaypipes): Clearly this is applying the same assertion to each - // object that was applied, which is wrong. When I add the polymorphism - // to the Assertions struct, I will modify this block to look for an - // indexed set of error assertions. - a = newAssertions(s.Assert, err, obj) - return result.New(result.WithFailures(a.Failures()...)) - } - return nil -} - -// unstructuredFromReader attempts to read the supplied io.Reader and unmarshal -// the content into zero or more unstructured.Unstructured objects -func unstructuredFromReader( - r io.Reader, -) ([]*unstructured.Unstructured, error) { - yr := yaml.NewYAMLReader(bufio.NewReader(r)) - - objs := []*unstructured.Unstructured{} - for { - raw, err := yr.Read() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - data := parse.ExpandWithFixedDoubleDollar(string(raw)) - - obj := &unstructured.Unstructured{} - decoder := yaml.NewYAMLOrJSONDecoder( - bytes.NewBuffer([]byte(data)), len(data), - ) - if err = decoder.Decode(obj); err != nil { - return nil, err - } - if obj.GetObjectKind().GroupVersionKind().Kind != "" { - objs = append(objs, obj) - } - } - - return objs, nil -} - -// delete executes either Delete() call against the Kubernetes API server -// and evaluates any assertions that have been set for the returned results. -func (s *Spec) delete( - ctx context.Context, - t *testing.T, - c *connection, -) *result.Result { - if s.Kube.Delete.FilePath() != "" { - path := s.Kube.Delete.FilePath() - f, err := os.Open(path) - if err != nil { - // This should never happen because we check during parse time - // whether the file can be opened. - rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) - return result.New(result.WithRuntimeError(rterr)) - } - defer f.Close() - objs, err := unstructuredFromReader(f) - if err != nil { - rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err) - return result.New(result.WithRuntimeError(rterr)) - } - for _, obj := range objs { - gvk := obj.GetObjectKind().GroupVersionKind() - res, err := c.gvrFromGVK(gvk) - a := newAssertions(s.Assert, err, nil) - if !a.OK() { - return result.New(result.WithFailures(a.Failures()...)) - } - name := obj.GetName() - ns := obj.GetNamespace() - if ns == "" { - ns = s.Namespace() - } - // TODO(jaypipes): Clearly this is applying the same assertion to each - // object that was deleted, which is wrong. When I add the polymorphism - // to the Assertions struct, I will modify this block to look for an - // indexed set of error assertions. - r := s.doDelete(ctx, t, c, res, name, ns) - if len(r.Failures()) > 0 { - return r - } - } - return result.New() - } - - kind, name := s.Kube.Delete.KindName() - gvk := schema.GroupVersionKind{ - Kind: kind, - } - res, err := c.gvrFromGVK(gvk) - a := newAssertions(s.Assert, err, nil) - if !a.OK() { - return result.New(result.WithFailures(a.Failures()...)) - } - if name == "" { - return s.doDeleteCollection(ctx, t, c, res, s.Namespace()) - } - return s.doDelete(ctx, t, c, res, name, s.Namespace()) -} - -// doDelete performs the Delete() call and assertion check for a supplied -// resource kind and name -func (s *Spec) doDelete( - ctx context.Context, - t *testing.T, - c *connection, - res schema.GroupVersionResource, - name string, - namespace string, -) *result.Result { - err := c.client.Resource(res).Namespace(namespace).Delete( - ctx, - name, - metav1.DeleteOptions{}, - ) - a := newAssertions(s.Assert, err, nil) - return result.New(result.WithFailures(a.Failures()...)) -} - -// doDeleteCollection performs the DeleteCollection() call and assertion check -// for a supplied resource kind -func (s *Spec) doDeleteCollection( - ctx context.Context, - t *testing.T, - c *connection, - res schema.GroupVersionResource, - namespace string, -) *result.Result { - listOpts := metav1.ListOptions{} - withlabels := s.Kube.Delete.Labels() - if withlabels != nil { - // We already validated the label selector during parse-time - listOpts.LabelSelector = labels.Set(withlabels).String() - } - err := c.client.Resource(res).Namespace(namespace).DeleteCollection( - ctx, - metav1.DeleteOptions{}, - listOpts, - ) - a := newAssertions(s.Assert, err, nil) - return result.New(result.WithFailures(a.Failures()...)) -} diff --git a/identifier.go b/identifier.go index 3242cec..3c87baf 100644 --- a/identifier.go +++ b/identifier.go @@ -191,3 +191,11 @@ func NewResourceIdentifierOrFile( labels: labels, } } + +// splitKindName returns the Kind for a supplied `Get` or `Delete` command +// where the user can specify either a resource kind or alias, e.g. "pods" or +// "po", or the resource kind followed by a forward slash and a resource name. +func splitKindName(subject string) (string, string) { + kind, name, _ := strings.Cut(subject, "/") + return kind, name +} diff --git a/spec.go b/spec.go index 7dd44ef..930cbc4 100644 --- a/spec.go +++ b/spec.go @@ -37,6 +37,7 @@ type KubeSpec struct { // Spec describes a test of a *single* Kubernetes API request and response. type Spec struct { gdttypes.Spec + Action // Kube is the complex type containing all of the Kubernetes-specific // actions and assertions. Most users will use the `kube.create`, // `kube.apply` and `kube.describe` shortcut fields.