From 464030a3aac125a372a9027b4ce73a55af346994 Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Tue, 25 Jun 2024 12:02:02 -0400 Subject: [PATCH] ensure no retry for create/apply/delete 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 --- action.go | 14 ++++---- assertions.go | 29 ++++++++-------- defaults.go | 11 +++--- errors.go | 32 +++++++++--------- eval.go | 15 ++++---- fixtures/kind/kind.go | 76 ++++++++++++++++++++++++++++++++++++++--- go.mod | 2 +- go.sum | 4 +-- identifier.go | 9 +++-- parse.go | 79 +++++++++++++++++++++---------------------- parse_test.go | 79 +++++++++++++++++++++---------------------- plugin.go | 16 ++++----- spec.go | 26 +++++++++++--- 13 files changed, 235 insertions(+), 157 deletions(-) diff --git a/action.go b/action.go index d823e8c..3d1d97f 100644 --- a/action.go +++ b/action.go @@ -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" @@ -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() @@ -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 { @@ -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() @@ -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 { @@ -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 { diff --git a/assertions.go b/assertions.go index 96bb89b..37daeb9 100644 --- a/assertions.go +++ b/assertions.go @@ -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" @@ -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 @@ -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) } @@ -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 @@ -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 @@ -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 @@ -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 } } @@ -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 // } //} @@ -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 // } //} @@ -511,7 +510,7 @@ func newAssertions( exp *Expect, err error, r interface{}, -) gdttypes.Assertions { +) api.Assertions { return &assertions{ c: c, failures: []error{}, diff --git a/defaults.go b/defaults.go index cdf2dcd..9405ad4 100644 --- a/defaults.go +++ b/defaults.go @@ -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" ) @@ -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 { @@ -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 } diff --git a/errors.go b/errors.go index f6a4607..f5c3c9a 100644 --- a/errors.go +++ b/errors.go @@ -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" ) @@ -20,7 +20,7 @@ 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 @@ -28,7 +28,7 @@ var ( 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 @@ -36,46 +36,46 @@ var ( 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 @@ -83,7 +83,7 @@ var ( 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 @@ -91,32 +91,32 @@ var ( // 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, ) ) diff --git a/eval.go b/eval.go index 618e751..434984c 100644 --- a/eval.go +++ b/eval.go @@ -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) @@ -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 } diff --git a/fixtures/kind/kind.go b/fixtures/kind/kind.go index 8b6e5a7..382c918 100644 --- a/fixtures/kind/kind.go +++ b/fixtures/kind/kind.go @@ -7,18 +7,28 @@ package kind import ( "context" "strings" + "time" + "github.com/cenkalti/backoff" + "github.com/gdt-dev/gdt/api" gdtcontext "github.com/gdt-dev/gdt/context" "github.com/gdt-dev/gdt/debug" - gdttypes "github.com/gdt-dev/gdt/types" "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/kind/pkg/cluster" kindconst "sigs.k8s.io/kind/pkg/cluster/constants" gdtkube "github.com/gdt-dev/kube" ) -// KindFixture implements `gdttypes.Fixture` and exposes connection/config +var ( + checkDefaultServiceAccountTimeout = time.Second * 15 +) + +// KindFixture implements `api.Fixture` and exposes connection/config // information about a running KinD cluster. type KindFixture struct { // provider is the KinD cluster provider @@ -68,7 +78,7 @@ func (f *KindFixture) Start(ctx context.Context) error { if f.isRunning() { debug.Println(ctx, "cluster %s already running", f.ClusterName) f.runningBeforeStart = true - return nil + return f.waitForDefaultServiceAccount(ctx) } opts := []cluster.CreateOption{} if f.ConfigPath != "" { @@ -86,7 +96,7 @@ func (f *KindFixture) Start(ctx context.Context) error { f.deleteOnStop = true debug.Println(ctx, "cluster %s will be deleted on stop", f.ClusterName) } - return nil + return f.waitForDefaultServiceAccount(ctx) } func (f *KindFixture) isRunning() bool { @@ -100,6 +110,62 @@ func (f *KindFixture) isRunning() bool { return lo.Contains(clusterNames, f.ClusterName) } +func (f *KindFixture) waitForDefaultServiceAccount(ctx context.Context) error { + // Sometimes it takes a little while for the default service account to + // exist for new clusters, and the default service account is required for + // a lot of testing, so we wait here until the default service account is + // ready to go... + cfg, err := f.provider.KubeConfig(f.ClusterName, false) + if err != nil { + return err + } + cc, err := clientcmd.Load([]byte(cfg)) + if err != nil { + return err + } + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, checkDefaultServiceAccountTimeout) + defer cancel() + overrides := &clientcmd.ConfigOverrides{} + rules := clientcmd.NewDefaultClientConfigLoadingRules() + ccfg, err := clientcmd.NewNonInteractiveClientConfig( + *cc, "", overrides, rules, + ).ClientConfig() + if err != nil { + return err + } + clientset, err := kubernetes.NewForConfig(ccfg) + if err != nil { + return err + } + bo := backoff.WithContext( + backoff.NewExponentialBackOff(), + ctx, + ) + ticker := backoff.NewTicker(bo) + attempts := 1 + for _ = range ticker.C { + found := true + _, err = clientset.CoreV1().ServiceAccounts("default").Get(context.TODO(), "default", metav1.GetOptions{}) + if err != nil { + if !errors.IsNotFound(err) { + return err + } + found = false + } + debug.Println( + ctx, "check for default service account: attempt %d, found: %v", + attempts, found, + ) + attempts++ + if found { + ticker.Stop() + break + } + } + return nil +} + func (f *KindFixture) Stop(ctx context.Context) { ctx = gdtcontext.PushTrace(ctx, "fixtures.kind.stop") defer func() { @@ -216,7 +282,7 @@ func WithRetainOnStop() KindFixtureModifier { // - "kube.config" returns the path of the kubeconfig file to use with this // KinD cluster // - "kube.context" returns the kubecontext to use with this KinD cluster -func New(mods ...KindFixtureModifier) gdttypes.Fixture { +func New(mods ...KindFixtureModifier) api.Fixture { f := &KindFixture{ provider: cluster.NewProvider(), } diff --git a/go.mod b/go.mod index 9e4abb0..2176ca0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gdt-dev/kube go 1.21 require ( - github.com/gdt-dev/gdt v1.8.0 + github.com/gdt-dev/gdt v1.9.0 github.com/samber/lo v1.38.1 github.com/stretchr/testify v1.8.4 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 643a84b..c11d950 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/gdt-dev/gdt v1.8.0 h1:JWuRbvSmVdNMQ8qHw1LZqVsQZEL4oM3mr85Z1L48vwQ= -github.com/gdt-dev/gdt v1.8.0/go.mod h1:oph7/YpGDMhnOz2TkNyrllpAiwmGaMyFjUAXhAGNZHI= +github.com/gdt-dev/gdt v1.9.0 h1:OoWTEXaMEze4EMyx1l9bu3DPngA3iawWuExui+k8Kn0= +github.com/gdt-dev/gdt v1.9.0/go.mod h1:oph7/YpGDMhnOz2TkNyrllpAiwmGaMyFjUAXhAGNZHI= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/identifier.go b/identifier.go index 3c87baf..47184e7 100644 --- a/identifier.go +++ b/identifier.go @@ -8,10 +8,9 @@ import ( "path/filepath" "strings" + "github.com/gdt-dev/gdt/api" "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/labels" - - gdterrors "github.com/gdt-dev/gdt/errors" ) // resourceIdentifierWithSelector is the full long-form resource identifier as @@ -56,7 +55,7 @@ func (r *ResourceIdentifier) Labels() map[string]string { // ResourceIdentifier can be either a string or a selector. func (r *ResourceIdentifier) UnmarshalYAML(node *yaml.Node) error { if node.Kind != yaml.ScalarNode && node.Kind != yaml.MappingNode { - return gdterrors.ExpectedScalarOrMapAt(node) + return api.ExpectedScalarOrMapAt(node) } var s string // A resource identifier can be a string of the form {type}/{name} or @@ -140,7 +139,7 @@ func (r *ResourceIdentifierOrFile) Labels() map[string]string { // ResourceIdentifierOrFile can be either a string or a selector. func (r *ResourceIdentifierOrFile) UnmarshalYAML(node *yaml.Node) error { if node.Kind != yaml.ScalarNode && node.Kind != yaml.MappingNode { - return gdterrors.ExpectedScalarOrMapAt(node) + return api.ExpectedScalarOrMapAt(node) } var s string // A resource identifier can be a filepath, a string of the form @@ -148,7 +147,7 @@ func (r *ResourceIdentifierOrFile) UnmarshalYAML(node *yaml.Node) error { if err := node.Decode(&s); err == nil { if probablyFilePath(s) { if !fileExists(s) { - return gdterrors.FileNotFound(s, node) + return api.FileNotFound(s, node) } r.fp = s return nil diff --git a/parse.go b/parse.go index 3e3b022..eaa42e2 100644 --- a/parse.go +++ b/parse.go @@ -7,16 +7,15 @@ package kube import ( "os" + "github.com/gdt-dev/gdt/api" gdtjson "github.com/gdt-dev/gdt/assertion/json" - "github.com/gdt-dev/gdt/errors" - gdttypes "github.com/gdt-dev/gdt/types" "github.com/samber/lo" "gopkg.in/yaml.v3" ) func (s *Spec) UnmarshalYAML(node *yaml.Node) error { if node.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(node) + return api.ExpectedMapAt(node) } // We do an initial pass over the shortcut fields, then all the // non-shortcut fields after that. @@ -27,14 +26,14 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { 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.get": if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.MappingNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } if ks != nil { return MoreThanOneKubeActionAt(valNode) @@ -48,7 +47,7 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { s.Kube = ks case "kube.create": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } if ks != nil { return MoreThanOneKubeActionAt(valNode) @@ -56,7 +55,7 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { v := valNode.Value if probablyFilePath(v) { if !fileExists(v) { - return errors.FileNotFound(v, valNode) + return api.FileNotFound(v, valNode) } } ks = &KubeSpec{} @@ -64,7 +63,7 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { s.Kube = ks case "kube.apply": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } if ks != nil { return MoreThanOneKubeActionAt(valNode) @@ -75,7 +74,7 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { s.Kube = ks case "kube.delete": if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.MappingNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } if ks != nil { return MoreThanOneKubeActionAt(valNode) @@ -93,14 +92,14 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { 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) } if ks != nil { return EitherShortcutOrKubeSpecAt(valNode) @@ -111,7 +110,7 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { s.Kube = ks case "assert": if valNode.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(valNode) + return api.ExpectedMapAt(valNode) } var e *Expect if err := valNode.Decode(&e); err != nil { @@ -121,10 +120,10 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { case "kube.get", "kube.create", "kube.delete", "kube.apply": continue default: - if lo.Contains(gdttypes.BaseSpecFields, key) { + if lo.Contains(api.BaseSpecFields, key) { continue } - return errors.UnknownFieldAt(key, keyNode) + return api.UnknownFieldAt(key, keyNode) } } return nil @@ -132,30 +131,30 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error { func (s *KubeSpec) 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 "config": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } fp := valNode.Value if !fileExists(fp) { - return errors.FileNotFound(fp, valNode) + return api.FileNotFound(fp, valNode) } s.Config = fp case "context": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } // NOTE(jaypipes): We can't validate the kubectx exists yet because // fixtures may advertise a kube config and we look up the context @@ -163,14 +162,14 @@ func (s *KubeSpec) UnmarshalYAML(node *yaml.Node) error { s.Context = valNode.Value case "namespace": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } s.Namespace = valNode.Value case "get", "create", "apply", "delete": // Because Action is an embedded struct and we parse it below, just // ignore these fields in the top-level `kube:` field for now. default: - return errors.UnknownFieldAt(key, keyNode) + return api.UnknownFieldAt(key, keyNode) } } var a Action @@ -183,43 +182,43 @@ func (s *KubeSpec) UnmarshalYAML(node *yaml.Node) error { func (a *Action) 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 "apply": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } v := valNode.Value if probablyFilePath(v) { if !fileExists(v) { - return errors.FileNotFound(v, valNode) + return api.FileNotFound(v, valNode) } } a.Apply = v case "create": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } v := valNode.Value if probablyFilePath(v) { if !fileExists(v) { - return errors.FileNotFound(v, valNode) + return api.FileNotFound(v, valNode) } } a.Create = v case "get": if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.MappingNode { - return errors.ExpectedScalarOrMapAt(valNode) + return api.ExpectedScalarOrMapAt(valNode) } var v *ResourceIdentifier if err := valNode.Decode(&v); err != nil { @@ -228,7 +227,7 @@ func (a *Action) UnmarshalYAML(node *yaml.Node) error { a.Get = v case "delete": if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.MappingNode { - return errors.ExpectedScalarOrMapAt(valNode) + return api.ExpectedScalarOrMapAt(valNode) } var v *ResourceIdentifierOrFile if err := valNode.Decode(&v); err != nil { @@ -245,21 +244,21 @@ func (a *Action) UnmarshalYAML(node *yaml.Node) error { func (e *Expect) 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 "error": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } var v string if err := valNode.Decode(&v); err != nil { @@ -268,7 +267,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { e.Error = v case "len": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } var v *int if err := valNode.Decode(&v); err != nil { @@ -277,7 +276,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { e.Len = v case "unknown": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } var v bool if err := valNode.Decode(&v); err != nil { @@ -286,7 +285,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { e.Unknown = v case "notfound": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return api.ExpectedScalarAt(valNode) } var v bool if err := valNode.Decode(&v); err != nil { @@ -295,7 +294,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { e.NotFound = v case "json": if valNode.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(valNode) + return api.ExpectedMapAt(valNode) } var v *gdtjson.Expect if err := valNode.Decode(&v); err != nil { @@ -304,7 +303,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { e.JSON = v case "conditions": if valNode.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(valNode) + return api.ExpectedMapAt(valNode) } var v map[string]*ConditionMatch if err := valNode.Decode(&v); err != nil { @@ -328,7 +327,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { } if probablyFilePath(v) { if !fileExists(v) { - return errors.FileNotFound(v, valNode) + return api.FileNotFound(v, valNode) } } // inline YAML. check it can be unmarshaled into a @@ -343,7 +342,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { } case "placement": if valNode.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(valNode) + return api.ExpectedMapAt(valNode) } var v *PlacementAssertion if err := valNode.Decode(&v); err != nil { @@ -351,7 +350,7 @@ func (e *Expect) UnmarshalYAML(node *yaml.Node) error { } e.Placement = v default: - return errors.UnknownFieldAt(key, keyNode) + return api.UnknownFieldAt(key, keyNode) } } return nil diff --git a/parse_test.go b/parse_test.go index a64fedb..042158e 100644 --- a/parse_test.go +++ b/parse_test.go @@ -10,8 +10,7 @@ import ( "testing" "github.com/gdt-dev/gdt" - "github.com/gdt-dev/gdt/errors" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/gdt/api" gdtkube "github.com/gdt-dev/kube" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,7 +29,7 @@ func TestFailureBadDefaults(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) - assert.ErrorIs(err, errors.ErrExpectedMap) + assert.ErrorIs(err, api.ErrExpectedMap) require.Nil(s) } @@ -43,7 +42,7 @@ func TestFailureDefaultsConfigNotFound(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrKubeConfigNotFound) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -56,7 +55,7 @@ func TestFailureBothShortcutAndKubeSpec(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrEitherShortcutOrKubeSpec) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -69,7 +68,7 @@ func TestFailureMoreThanOneKubeAction(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrMoreThanOneKubeAction) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -82,7 +81,7 @@ func TestFailureInvalidResourceSpecifierNoMultipleResources(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrResourceSpecifierInvalid) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -95,7 +94,7 @@ func TestFailureInvalidResourceSpecifierMutipleForwardSlashes(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrResourceSpecifierInvalid) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -108,7 +107,7 @@ func TestFailureInvalidDeleteNotFilepathOrResourceSpecifier(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrResourceSpecifierInvalidOrFilepath) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -120,8 +119,8 @@ func TestFailureCreateFileNotFound(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) - assert.ErrorIs(err, errors.ErrFileNotFound) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrFileNotFound) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -133,8 +132,8 @@ func TestFailureDeleteFileNotFound(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) - assert.ErrorIs(err, errors.ErrFileNotFound) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrFileNotFound) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -146,8 +145,8 @@ func TestFailureBadMatchesFileNotFound(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) - assert.ErrorIs(err, errors.ErrFileNotFound) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrFileNotFound) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -160,7 +159,7 @@ func TestFailureBadMatchesInvalidYAML(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrMatchesInvalid) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -173,7 +172,7 @@ func TestFailureBadMatchesEmpty(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrExpectedMapOrYAMLString) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -186,7 +185,7 @@ func TestFailureBadMatchesNotMapAny(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrMatchesInvalid) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -198,8 +197,8 @@ func TestFailureBadPlacementNotObject(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) - assert.ErrorIs(err, errors.ErrExpectedMap) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrExpectedMap) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -212,7 +211,7 @@ func TestWithLabelsInvalid(t *testing.T) { s, err := gdt.From(fp) require.NotNil(err) assert.ErrorIs(err, gdtkube.ErrWithLabelsInvalid) - assert.ErrorIs(err, errors.ErrParse) + assert.ErrorIs(err, api.ErrParse) require.Nil(s) } @@ -242,12 +241,12 @@ spec: ` var zero int - expTests := []gdttypes.Evaluable{ + expTests := []api.Evaluable{ &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 0, Name: "create a pod from YAML using kube.create shortcut", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -256,10 +255,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 1, Name: "apply a pod from a file using kube.apply shortcut", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -268,10 +267,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 2, Name: "create a pod from YAML", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -280,10 +279,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 3, Name: "delete a pod from a file", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -295,10 +294,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 4, Name: "fetch a pod via kube.get shortcut", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -309,10 +308,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 5, Name: "fetch a pod via long-form kube:get", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -323,10 +322,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 6, Name: "fetch a pod via kube.get shortcut to long-form resource identifier with labels", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -339,10 +338,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 7, Name: "fetch a pod via kube:get long-form resource identifier with labels", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ @@ -355,10 +354,10 @@ spec: }, }, &gdtkube.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 8, Name: "fetch a pod with envvar substitution", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, Kube: &gdtkube.KubeSpec{ Action: gdtkube.Action{ diff --git a/plugin.go b/plugin.go index 8cb097a..de92724 100644 --- a/plugin.go +++ b/plugin.go @@ -6,7 +6,7 @@ package kube import ( "github.com/gdt-dev/gdt" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/gdt/api" "gopkg.in/yaml.v3" ) @@ -27,13 +27,13 @@ const ( type plugin struct{} -func (p *plugin) Info() gdttypes.PluginInfo { - return gdttypes.PluginInfo{ +func (p *plugin) Info() api.PluginInfo { + return api.PluginInfo{ Name: pluginName, - Retry: &gdttypes.Retry{ + Retry: &api.Retry{ Exponential: true, }, - Timeout: &gdttypes.Timeout{ + Timeout: &api.Timeout{ After: DefaultTimeout, }, } @@ -43,11 +43,11 @@ func (p *plugin) Defaults() yaml.Unmarshaler { return &Defaults{} } -func (p *plugin) Specs() []gdttypes.Evaluable { - return []gdttypes.Evaluable{&Spec{}} +func (p *plugin) Specs() []api.Evaluable { + return []api.Evaluable{&Spec{}} } // Plugin returns the Kubernetes gdt plugin -func Plugin() gdttypes.Plugin { +func Plugin() api.Plugin { return &plugin{} } diff --git a/spec.go b/spec.go index 7dd44ef..75e3214 100644 --- a/spec.go +++ b/spec.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/gdt/api" ) // KubeSpec is the complex type containing all of the Kubernetes-specific @@ -36,7 +36,7 @@ type KubeSpec struct { // Spec describes a test of a *single* Kubernetes API request and response. type Spec struct { - gdttypes.Spec + api.Spec // 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. @@ -82,6 +82,24 @@ type Spec struct { Assert *Expect `yaml:"assert,omitempty"` } +func (s *Spec) Retry() *api.Retry { + if s.Spec.Retry != nil { + // The user may have overridden in the test spec file... + return s.Spec.Retry + } + if s.Kube.Action.Get != nil { + // returning nil here means the plugin's default will be used... + return nil + } + // for apply/create/delete, we don't want to retry... + return api.NoRetry +} + +func (s *Spec) Timeout() *api.Timeout { + // returning nil here means the plugin's default will be used... + return nil +} + // Title returns a good name for the Spec func (s *Spec) Title() string { // If the user did not specify a name for the test spec, just default @@ -123,11 +141,11 @@ func probablyFilePath(subject string) bool { return strings.ContainsRune(subject, '.') } -func (s *Spec) SetBase(b gdttypes.Spec) { +func (s *Spec) SetBase(b api.Spec) { s.Spec = b } -func (s *Spec) Base() *gdttypes.Spec { +func (s *Spec) Base() *api.Spec { return &s.Spec }