Skip to content
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
88 changes: 85 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,16 @@ matches some expectation:
* `kube`: (optional) an object containing actions and assertions the test takes
against the Kubernetes API server.
* `kube.get`: (optional) string or object containing a resource identifier
(e.g. `pods`, `po/nginx` or label selector for resources that will be read
from the Kubernetes API server.
(e.g. `pods`, `po/nginx` or [label selector](#using-label-selectors) for
resources that will be read from the Kubernetes API server.
* `kube.create`: (optional) string containing either a file path to a YAML
manifest or a string of raw YAML containing the resource(s) to create.
* `kube.apply`: (optional) string containing either a file path to a YAML
manifest or a string of raw YAML containing the resource(s) for which
`gdt-kube` will perform a Kubernetes Apply call.
* `kube.delete`: (optional) string or object containing either a resource
identifier (e.g. `pods`, `po/nginx` , a file path to a YAML manifest, or a
label selector for resources that will be deleted.
[label selector](#using-label-selectors) for resources that will be deleted.
* `var`: (optional) an object describing variables that can have
values saved and referred to by subsequent test specs. Each key in the `var`
object is the name of the variable to define.
Expand Down Expand Up @@ -347,6 +347,88 @@ tests:
- kube.delete: pods/nginx
```

### Using label selectors

When selecting objects from the Kubernetes API, you can use a label selector in
the `kube.get` and `kube.delete` test spec actions. This label selector
functionality is incredibly flexible. You can use a `kubectl`-style label
selector string, like so:

```yaml
- name: select pods that have the "app=argo" label but do NOT have the "app=argo-rollouts" or "app=argorollouts" label
kube:
get:
type: pod
labels: app in (argo),app notin (argo-rollouts,argorollouts)
```

The `kube.get.labels` field can also be a map of string to string, which is
more aligned with how `gdt`'s YAML syntax is structured. This example selects
pods that have **both** the `app=myapp` **and** the `region=myregion` label:

```yaml
- name: select pods that have BOTH the app=myapp AND region=myregion label
kube:
get:
type: pod
labels:
app: myapp
region: myregion
```

The `kube.get.labels-in` field is a map of string to slice of strings and gets
translated into a "label IN (val1, val2)" expression. This example selects pods
that have **either** the `app=myapp` label **or** the `app=test` label:

```yaml
- name: select pods that have EITHER the app=myapp OR app=test label
kube:
get:
type: pod
labels-in:
app:
- myapp
- test
```

The `kube.get.labels-not-in` field is also a map of string to slice of strings
and gets translated into a `label NOTIN (val1, val2)` expression. This example
selects pods that **do not** have either the `app=myapp` or the `app=test`
label.

```yaml
- name: select pods that have DON'T HAVE the app=myapp OR app=test label
kube:
get:
type: pod
labels-not-in:
app:
- myapp
- test
```

You can combine the `kube.get.labels`, `kube.get.labels-in` and
`kube.get.labels-not-in` fields to create complex querying expressions. This
example selects pods that have the `app=myapp` label **and** have **either**
the `category=test` or `category=staging` label **and** do **not** have a
`personal=true` label:

```yaml
- name: select pods that have have an app=myapp label AND have either a category=test or category=staging label AND do not have the personal=true label
kube:
get:
type: pod
labels:
app: myapp
labels-in:
category:
- test
- staging
labels-not-in:
personal:
- true
```

### Passing variables to subsequent test specs

A `gdt` test scenario is comprised of a list of test specs. These test specs
Expand Down
13 changes: 6 additions & 7 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/gdt-dev/core/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"
)
Expand Down Expand Up @@ -169,10 +168,10 @@ func (a *Action) doList(
resName := res.Resource
labelSelString := ""
opts := metav1.ListOptions{}
withlabels := a.Get.Labels
if withlabels != nil {
sel := a.Get.LabelSelector
if sel != nil {
// We already validated the label selector during parse-time
labelsStr := labels.Set(withlabels).String()
labelsStr := sel.String()
labelSelString = fmt.Sprintf(" (labels: %s)", labelsStr)
opts.LabelSelector = labelsStr
}
Expand Down Expand Up @@ -484,11 +483,11 @@ func (a *Action) doDeleteCollection(
ns string,
) error {
opts := metav1.ListOptions{}
withlabels := a.Delete.Labels
labelSelString := ""
if withlabels != nil {
sel := a.Delete.LabelSelector
if sel != nil {
// We already validated the label selector during parse-time
labelsStr := labels.Set(withlabels).String()
labelsStr := sel.String()
labelSelString = fmt.Sprintf(" (labels: %s)", labelsStr)
opts.LabelSelector = labelsStr
}
Expand Down
67 changes: 49 additions & 18 deletions identifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package kube

import (
"path/filepath"

kubelabels "k8s.io/apimachinery/pkg/labels"
)

// resourceIdentifierWithSelector is the full long-form resource identifier as
Expand All @@ -19,15 +21,22 @@ type resourceIdentifierWithSelector struct {
// Labels is a map, keyed by metadata Label, of Label values to select a
// resource by
Labels map[string]string `yaml:"labels,omitempty"`
// LabelsIn is a map, keyed by metadata Label, of slices of string label
// values to select a resource with an IN() selector.
LabelsIn map[string][]string `yaml:"labels-in,omitempty"`
// LabelsNotIn is a map, keyed by metadata Label, of slices of string label
// values to select a resource with an NOT IN() selector.
LabelsNotIn map[string][]string `yaml:"labels-not-in,omitempty"`
LabelSelector kubelabels.Selector `yaml:"-"`
}

// ResourceIdentifier is a struct used to parse an interface{} that can be
// either a string or a struct containing a selector with things like a label
// key/value map.
type ResourceIdentifier struct {
Arg string `yaml:"-"`
Name string `yaml:"-"`
Labels map[string]string `yaml:"-"`
Arg string `yaml:"-"`
Name string `yaml:"-"`
LabelSelector kubelabels.Selector `yaml:"-"`
}

// Title returns the resource identifier's kind and name, if present
Expand All @@ -42,22 +51,33 @@ func NewResourceIdentifier(
arg string,
name string,
labels map[string]string,
) *ResourceIdentifier {
return &ResourceIdentifier{
Arg: arg,
Name: name,
Labels: labels,
) (*ResourceIdentifier, error) {
sel := kubelabels.Everything()
if len(labels) > 0 {
ls, err := kubelabels.ValidatedSelectorFromSet(labels)
if err != nil {
return nil, err
}
sel = ls
}
ri := &ResourceIdentifier{
Arg: arg,
Name: name,
}
if !sel.Empty() {
ri.LabelSelector = sel.DeepCopySelector()
}
return ri, nil
}

// ResourceIdentifierOrFile is a struct used to parse an interface{} that can
// be either a string, a filepath or a struct containing a selector with things
// like a label key/value map.
type ResourceIdentifierOrFile struct {
fp string `yaml:"-"`
Arg string `yaml:"-"`
Name string `yaml:"-"`
Labels map[string]string `yaml:"-"`
fp string `yaml:"-"`
Arg string `yaml:"-"`
Name string `yaml:"-"`
LabelSelector kubelabels.Selector `yaml:"-"`
}

// FilePath returns the resource identifier's file path, if present
Expand All @@ -82,11 +102,22 @@ func NewResourceIdentifierOrFile(
arg string,
name string,
labels map[string]string,
) *ResourceIdentifierOrFile {
return &ResourceIdentifierOrFile{
fp: fp,
Arg: arg,
Name: name,
Labels: labels,
) (*ResourceIdentifierOrFile, error) {
sel := kubelabels.Everything()
if len(labels) > 0 {
ls, err := kubelabels.ValidatedSelectorFromSet(labels)
if err != nil {
return nil, err
}
sel = ls
}
ri := &ResourceIdentifierOrFile{
fp: fp,
Arg: arg,
Name: name,
}
if !sel.Empty() {
ri.LabelSelector = sel.DeepCopySelector()
}
return ri, nil
}
113 changes: 102 additions & 11 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import (
"github.com/samber/lo"
"github.com/theory/jsonpath"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/labels"
kubelabels "k8s.io/apimachinery/pkg/labels"
kubeselection "k8s.io/apimachinery/pkg/selection"
)

// EitherShortcutOrKubeSpecAt returns a parse error indicating the test author
Expand Down Expand Up @@ -561,13 +562,9 @@ func (r *ResourceIdentifier) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode(&ri); err != nil {
return err
}
_, err := labels.ValidatedSelectorFromSet(ri.Labels)
if err != nil {
return InvalidWithLabelsAt(err, node)
}
r.Arg = ri.Type
r.Name = ri.Name
r.Labels = ri.Labels
r.LabelSelector = ri.LabelSelector
return nil
}

Expand Down Expand Up @@ -603,13 +600,107 @@ func (r *ResourceIdentifierOrFile) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode(&ri); err != nil {
return err
}
_, err := labels.ValidatedSelectorFromSet(ri.Labels)
if err != nil {
return InvalidWithLabelsAt(err, node)
}
r.Arg = ri.Type
r.Name = ri.Name
r.Labels = ri.Labels
r.LabelSelector = ri.LabelSelector
return nil
}

func (r *resourceIdentifierWithSelector) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return parse.ExpectedMapAt(node)
}
sel := kubelabels.Everything()
// 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 parse.ExpectedScalarAt(keyNode)
}
key := keyNode.Value
valNode := node.Content[i+1]
switch key {
case "type":
if valNode.Kind != yaml.ScalarNode {
return parse.ExpectedScalarAt(valNode)
}
r.Type = valNode.Value
case "name":
if valNode.Kind != yaml.ScalarNode {
return parse.ExpectedScalarAt(valNode)
}
r.Name = valNode.Value
case "labels", "labels-all", "labels_all":
if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.MappingNode {
return parse.ExpectedScalarOrMapAt(valNode)
}
if valNode.Kind == yaml.ScalarNode {
s, err := kubelabels.Parse(valNode.Value)
if err != nil {
return InvalidWithLabelsAt(err, valNode)
}
// NOTE(jaypipes): If the `labels` key was found and the value
// was a string that successfully parsed according to the
// kubectl labels selector format, ignore any other
// labels-in/labels-not-in keys.
r.LabelSelector = s.DeepCopySelector()
return nil
} else {
var m map[string]string
if err := valNode.Decode(&m); err != nil {
return err
}
s, err := kubelabels.ValidatedSelectorFromSet(m)
if err != nil {
return InvalidWithLabelsAt(err, valNode)
}
newReqs, _ := s.Requirements()
for _, req := range newReqs {
sel = sel.Add(req)
}
}
case "labels-in", "labels_in", "labels-any", "labels_any":
if valNode.Kind != yaml.MappingNode {
return parse.ExpectedMapAt(valNode)
}
var m map[string][]string
if err := valNode.Decode(&m); err != nil {
return err
}
for k, vals := range m {
req, err := kubelabels.NewRequirement(
k, kubeselection.In, vals,
)
if err != nil {
return InvalidWithLabelsAt(err, valNode)
}
sel = sel.Add(*req)
}
case "labels-not-in", "labels_not_in", "labels-not-any", "labels_not_any":
if valNode.Kind != yaml.MappingNode {
return parse.ExpectedMapAt(valNode)
}
var m map[string][]string
if err := valNode.Decode(&m); err != nil {
return err
}
for k, vals := range m {
req, err := kubelabels.NewRequirement(
k, kubeselection.NotIn, vals,
)
if err != nil {
return InvalidWithLabelsAt(err, valNode)
}
sel = sel.Add(*req)
}
default:
return parse.UnknownFieldAt(key, keyNode)
}
}
if !sel.Empty() {
r.LabelSelector = sel.DeepCopySelector()
}
return nil
}

Expand Down
Loading
Loading