Skip to content

Commit

Permalink
ensure no retry for create/apply/delete
Browse files Browse the repository at this point in the history
We only want to retry kube.get actions, not create, apply or delete. In
bringing in gdt@v1.9.0, we now have override ability for an individual
Evaluable's Retry and Timeout.

When adding this functionality, it became evident that KinD clusters
don't immediately add the default service account, which is required for
simple tests like the creation of an nginx Pod.

So, added a wait loop inside the KindFixture's Start method that waits
for up to 15 seconds until it sees the existence of the default service
account.

Issue #17

Signed-off-by: Jay Pipes <jaypipes@gmail.com>
  • Loading branch information
jaypipes committed Jun 25, 2024
1 parent 3e24bea commit 464030a
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 157 deletions.
14 changes: 7 additions & 7 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
"os"
"strings"

"github.com/gdt-dev/gdt/api"
"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"
Expand Down Expand Up @@ -226,7 +226,7 @@ func (a *Action) create(
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)
rterr := fmt.Errorf("%w: %s", api.RuntimeError, err)
return rterr
}
defer f.Close()
Expand All @@ -245,7 +245,7 @@ func (a *Action) create(

objs, err := unstructuredFromReader(r)
if err != nil {
rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err)
rterr := fmt.Errorf("%w: %s", api.RuntimeError, err)
return rterr
}
for _, obj := range objs {
Expand Down Expand Up @@ -290,7 +290,7 @@ func (a *Action) apply(
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)
rterr := fmt.Errorf("%w: %s", api.RuntimeError, err)
return rterr
}
defer f.Close()
Expand All @@ -309,7 +309,7 @@ func (a *Action) apply(

objs, err := unstructuredFromReader(r)
if err != nil {
rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err)
rterr := fmt.Errorf("%w: %s", api.RuntimeError, err)
return rterr
}
for _, obj := range objs {
Expand Down Expand Up @@ -358,13 +358,13 @@ func (a *Action) delete(
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)
rterr := fmt.Errorf("%w: %s", api.RuntimeError, err)
return rterr
}
defer f.Close()
objs, err := unstructuredFromReader(f)
if err != nil {
rterr := fmt.Errorf("%w: %s", gdterrors.RuntimeError, err)
rterr := fmt.Errorf("%w: %s", api.RuntimeError, err)
return rterr
}
for _, obj := range objs {
Expand Down
29 changes: 14 additions & 15 deletions assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import (
"net/http"
"strings"

"github.com/gdt-dev/gdt/api"
gdtjson "github.com/gdt-dev/gdt/assertion/json"
gdterrors "github.com/gdt-dev/gdt/errors"
gdttypes "github.com/gdt-dev/gdt/types"
"gopkg.in/yaml.v3"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -166,8 +165,8 @@ type Expect struct {
// conditionMatch is a struct with fields that we will match a resource's
// `Condition` against.
type conditionMatch struct {
Status *gdttypes.FlexStrings `yaml:"status,omitempty"`
Reason string `yaml:"reason,omitempty"`
Status *api.FlexStrings `yaml:"status,omitempty"`
Reason string `yaml:"reason,omitempty"`
}

// ConditionMatch can be a string (the ConditionStatus to match), a slice of
Expand All @@ -182,7 +181,7 @@ type ConditionMatch struct {
// ConditionMatch can be either a string, a slice of strings, or an object with .
func (m *ConditionMatch) UnmarshalYAML(node *yaml.Node) error {
if node.Kind == yaml.ScalarNode || node.Kind == yaml.SequenceNode {
var fs gdttypes.FlexStrings
var fs api.FlexStrings
if err := node.Decode(&fs); err != nil {
return ConditionMatchInvalid(node, err)
}
Expand All @@ -203,10 +202,10 @@ func (m *ConditionMatch) UnmarshalYAML(node *yaml.Node) error {
type PlacementAssertion struct {
// Spread contains zero or more topology keys that gdt-kube will assert an
// even spread across.
Spread *gdttypes.FlexStrings `yaml:"spread,omitempty"`
Spread *api.FlexStrings `yaml:"spread,omitempty"`
// Pack contains zero or more topology keys that gdt-kube will assert
// bin-packing of resources within.
Pack *gdttypes.FlexStrings `yaml:"pack,omitempty"`
Pack *api.FlexStrings `yaml:"pack,omitempty"`
}

// assertions contains all assertions made for the exec test
Expand Down Expand Up @@ -246,7 +245,7 @@ func (a *assertions) OK(ctx context.Context) bool {
exp := a.exp
if exp == nil {
if a.err != nil {
a.Fail(gdterrors.UnexpectedError(a.err))
a.Fail(api.UnexpectedError(a.err))
return false
}
return true
Expand Down Expand Up @@ -309,16 +308,16 @@ func (a *assertions) errorOK() bool {
}
if exp.Error != "" && a.r != nil {
if a.err == nil {
a.Fail(gdterrors.UnexpectedError(a.err))
a.Fail(api.UnexpectedError(a.err))
return false
}
if !strings.Contains(a.err.Error(), exp.Error) {
a.Fail(gdterrors.NotIn(a.err.Error(), exp.Error))
a.Fail(api.NotIn(a.err.Error(), exp.Error))
return false
}
}
if a.err != nil {
a.Fail(gdterrors.UnexpectedError(a.err))
a.Fail(api.UnexpectedError(a.err))
return false
}
return true
Expand Down Expand Up @@ -368,7 +367,7 @@ func (a *assertions) lenOK() bool {
list, ok := a.r.(*unstructured.UnstructuredList)
if ok && list != nil {
if len(list.Items) != *exp.Len {
a.Fail(gdterrors.NotEqualLength(*exp.Len, len(list.Items)))
a.Fail(api.NotEqualLength(*exp.Len, len(list.Items)))
return false
}
}
Expand Down Expand Up @@ -402,7 +401,7 @@ func (a *assertions) matchesOK() bool {
// for _, obj := range list.Items {
// diff := compareResourceToMatchObject(obj, matchObj)
//
// a.Fail(gdterrors.NotEqualLength(*exp.Len, len(list.Items)))
// a.Fail(api.NotEqualLength(*exp.Len, len(list.Items)))
// return false
// }
//}
Expand Down Expand Up @@ -435,7 +434,7 @@ func (a *assertions) conditionsOK() bool {
// for _, obj := range list.Items {
// diff := compareResourceToMatchObject(obj, matchObj)
//
// a.Fail(gdterrors.NotEqualLength(*exp.Len, len(list.Items)))
// a.Fail(api.NotEqualLength(*exp.Len, len(list.Items)))
// return false
// }
//}
Expand Down Expand Up @@ -511,7 +510,7 @@ func newAssertions(
exp *Expect,
err error,
r interface{},
) gdttypes.Assertions {
) api.Assertions {
return &assertions{
c: c,
failures: []error{},
Expand Down
11 changes: 5 additions & 6 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ package kube
import (
"os"

"github.com/gdt-dev/gdt/errors"
gdttypes "github.com/gdt-dev/gdt/types"
"github.com/gdt-dev/gdt/api"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -39,21 +38,21 @@ type Defaults struct {

func (d *Defaults) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return errors.ExpectedMapAt(node)
return api.ExpectedMapAt(node)
}
// maps/structs are stored in a top-level Node.Content field which is a
// concatenated slice of Node pointers in pairs of key/values.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
if keyNode.Kind != yaml.ScalarNode {
return errors.ExpectedScalarAt(keyNode)
return api.ExpectedScalarAt(keyNode)
}
key := keyNode.Value
valNode := node.Content[i+1]
switch key {
case "kube":
if valNode.Kind != yaml.MappingNode {
return errors.ExpectedMapAt(valNode)
return api.ExpectedMapAt(valNode)
}
hd := kubeDefaults{}
if err := valNode.Decode(&hd); err != nil {
Expand Down Expand Up @@ -86,7 +85,7 @@ func (d *Defaults) validate() error {
}

// fromBaseDefaults returns an gdt-kube plugin-specific Defaults from a Spec
func fromBaseDefaults(base *gdttypes.Defaults) *Defaults {
func fromBaseDefaults(base *api.Defaults) *Defaults {
if base == nil {
return nil
}
Expand Down
32 changes: 16 additions & 16 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package kube
import (
"fmt"

gdterrors "github.com/gdt-dev/gdt/errors"
"github.com/gdt-dev/gdt/api"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/runtime/schema"
)
Expand All @@ -20,103 +20,103 @@ var (
ErrExpectedMapOrYAMLString = fmt.Errorf(
"%w: expected either map[string]interface{} "+
"or a string with embedded YAML",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrEitherShortcutOrKubeSpec is returned when the test author
// included both a shortcut (e.g. `kube.create` or `kube.apply`) AND the
// long-form `kube` object in the same test spec.
ErrEitherShortcutOrKubeSpec = fmt.Errorf(
"%w: either specify a full KubeSpec in the `kube` field or specify "+
"one of the shortcuts (e.g. `kube.create` or `kube.apply`",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrMoreThanOneKubeAction is returned when the test author
// included more than one Kubernetes action (e.g. `create` or `apply`) in
// the same KubeSpec.
ErrMoreThanOneKubeAction = fmt.Errorf(
"%w: you may only specify a single Kubernetes action field "+
"(e.g. `create`, `apply` or `delete`) in the `kube` object. ",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrKubeConfigNotFound is returned when a kubeconfig path points
// to a file that does not exist.
ErrKubeConfigNotFound = fmt.Errorf(
"%w: specified kube config path not found",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrResourceSpecifier is returned when the test author uses a
// resource specifier for the `kube.get` or `kube.delete` fields that is
// not valid.
ErrResourceSpecifierInvalid = fmt.Errorf(
"%w: invalid resource specifier",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrResourceSpecifierOrFilepath is returned when the test author
// uses a resource specifier for the `kube.delete` fields that is not valid
// or is not a filepath.
ErrResourceSpecifierInvalidOrFilepath = fmt.Errorf(
"%w: invalid resource specifier or filepath",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrMatchesInvalid is returned when the `Kube.Assert.Matches` value is
// malformed.
ErrMatchesInvalid = fmt.Errorf(
"%w: `kube.assert.matches` not well-formed",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrConditionMatchInvalid is returned when the `Kube.Assert.Conditions`
// value is malformed.
ErrConditionMatchInvalid = fmt.Errorf(
"%w: `kube.assert.conditions` not well-formed",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrWithLabelsOnlyGetDelete is returned when the test author included
// `kube.with.labels` but did not specify either `kube.get` or
// `kube.delete`.
ErrWithLabelsInvalid = fmt.Errorf(
"%w: with labels invalid",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrWithLabelsOnlyGetDelete is returned when the test author included
// `kube.with.labels` but did not specify either `kube.get` or
// `kube.delete`.
ErrWithLabelsOnlyGetDelete = fmt.Errorf(
"%w: with labels may only be specified for "+
"`kube.get` or `kube.delete`",
gdterrors.ErrParse,
api.ErrParse,
)
// ErrResourceUnknown is returned when an unknown resource kind is
// specified for a create/apply/delete target. This is a runtime error
// because we rely on the discovery client to determine whether a resource
// kind is valid.
ErrResourceUnknown = fmt.Errorf(
"%w: resource unknown",
gdterrors.ErrFailure,
api.ErrFailure,
)
// ErrExpectedNotFound is returned when we expected to get either a
// NotFound response code (get) or an empty set of results (list) but did
// not find that.
ErrExpectedNotFound = fmt.Errorf(
"%w: expected not found",
gdterrors.ErrFailure,
api.ErrFailure,
)
// ErrMatchesNotEqual is returned when we failed to match a resource to an
// object field in a `kube.assert.matches` object.
ErrMatchesNotEqual = fmt.Errorf(
"%w: match field not equal",
gdterrors.ErrFailure,
api.ErrFailure,
)
// ErrConditionDoesNotMatch is returned when we failed to match a resource to an
// Condition match expression in a `kube.assert.matches` object.
ErrConditionDoesNotMatch = fmt.Errorf(
"%w: condition does not match expectation",
gdterrors.ErrFailure,
api.ErrFailure,
)
// ErrConnect is returned when we failed to create a client config to
// connect to the Kubernetes API server.
ErrConnect = fmt.Errorf(
"%w: k8s connect failure",
gdterrors.RuntimeError,
api.RuntimeError,
)
)

Expand Down
15 changes: 7 additions & 8 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ package kube
import (
"context"

gdterrors "github.com/gdt-dev/gdt/errors"
"github.com/gdt-dev/gdt/result"
"github.com/gdt-dev/gdt/api"
)

// 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) (*result.Result, error) {
func (s *Spec) Eval(ctx context.Context) (*api.Result, error) {
c, err := s.connect(ctx)
if err != nil {
return nil, ConnectError(err)
Expand All @@ -25,16 +24,16 @@ func (s *Spec) Eval(ctx context.Context) (*result.Result, error) {
var out interface{}
err = s.Kube.Do(ctx, c, ns, &out)
if err != nil {
if err == gdterrors.ErrTimeoutExceeded {
return result.New(result.WithFailures(gdterrors.ErrTimeoutExceeded)), nil
if err == api.ErrTimeoutExceeded {
return api.NewResult(api.WithFailures(api.ErrTimeoutExceeded)), nil
}
if err == gdterrors.RuntimeError {
if err == api.RuntimeError {
return nil, err
}
}
a := newAssertions(c, s.Assert, err, out)
if a.OK(ctx) {
return result.New(), nil
return api.NewResult(), nil
}
return result.New(result.WithFailures(a.Failures()...)), nil
return api.NewResult(api.WithFailures(a.Failures()...)), nil
}
Loading

0 comments on commit 464030a

Please sign in to comment.