kube

package module
v1.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2024 License: Apache-2.0 Imports: 37 Imported by: 0

README

Go Declarative Testing - Kubernetes

Go Reference Go Report Card Build Status Contributor Covenant

gdt is a testing library that allows test authors to cleanly describe tests in a YAML file. gdt reads YAML files that describe a test's assertions and then builds a set of Go structures that the standard Go testing package can execute.

This github.com/gdt-dev/kube (shortened hereafter to gdt-kube) repository is a companion Go library for gdt that allows test authors to cleanly describe functional tests of Kubernetes resources and actions using a simple, clear YAML format. gdt-kube parses YAML files that describe Kubernetes client/API requests and assertions about those client calls.

Usage

gdt-kube is a Go library and is intended to be included in your own Go application's test code as a Go package dependency.

Import the gdt and gdt-kube libraries in a Go test file:

import (
    "github.com/gdt-dev/gdt"
    gdtkube "github.com/gdt-dev/kube"
)

In a standard Go test function, use the gdt.From() function to instantiate a test object (either a Scenario or a Suite) that can be Run() with a standard Go context.Context and a standard Go *testing.T type:

func TestExample(t *testing.T) {
    s, err := gdt.From("path/to/test.yaml")
    if err != nil {
        t.Fatalf("failed to load tests: %s", err)
    }

    ctx := context.Background()
    err = s.Run(ctx, t)
    if err != nil {
        t.Fatalf("failed to run tests: %s", err)
    }
}

To execute the tests, just run go test per the standard Go testing practice.

gdt is a declarative testing framework and the meat of your tests is going to be in the YAML files that describe the actions and assertions for one or more tests. Read on for an explanation of how to write tests in this declarative YAML format.

gdt-kube test file structure

A gdt test scenario (or just "scenario") is simply a YAML file.

All gdt scenarios have the following fields:

  • name: (optional) string describing the contents of the test file. If missing or empty, the filename is used as the name
  • description: (optional) string with longer description of the test file contents
  • defaults: (optional) is a map, keyed by a plugin name, of default options and configuration values for that plugin.
  • fixtures: (optional) list of strings indicating named fixtures that will be started before any of the tests in the file are run
  • tests: list of Spec specializations that represent the runnable test units in the test scenario.
gdt-kube test configuration defaults

To set gdt-kube-specific default configuration values for the test scenario, set the defaults.kube field to an object containing any of these fields:

  • defaults.kube.config: (optional) file path to a kubeconfig to use for the test scenario.
  • defaults.kube.context: (optional) string containing the name of the kube context to use for the test scenario.
  • defaults.kube.namespace: (optional) string containing the Kubernetes namespace to use when performing some action for the test scenario.

As an example, let's say that I wanted to override the Kubernetes namespace and the kube context used for a particular test scenario. I would do the following:

name: example-test-with-defaults
defaults:
  kube:
    context: my-kube-context
    namespace: my-namespace
gdt-kube test spec structure

All gdt test specs have the same [base fields][base-spec-fields]:

  • name: (optional) string describing the test unit.
  • description: (optional) string with longer description of the test unit.
  • timeout: (optional) an object containing [timeout information][timeout] for the test unit.
  • timeout: (optional) a string duration of time the test unit is expected to complete within.
  • retry: (optional) an object containing retry configurationu for the test unit. Some plugins will automatically attempt to retry the test action when an assertion fails. This field allows you to control this retry behaviour for each individual test.
  • retry.interval: (optional) a string duration of time that the test plugin will retry the test action in the event assertions fail. The default interval for retries is plugin-dependent.
  • retry.attempts: (optional) an integer indicating the number of times that a plugin will retry the test action in the event assertions fail. The default number of attempts for retries is plugin-dependent.
  • retry.exponential: (optional) a boolean indicating an exponential backoff should be applied to the retry interval. The default is is plugin-dependent.
  • wait (optional) an object containing wait information for the test unit.
  • wait.before: a string duration of time that gdt should wait before executing the test unit's action.
  • wait.after: a string duration of time that gdt should wait after executing the test unit's action.

gdt-kube test specs have some additional fields that allow you to take some action against a Kubernetes API and assert that the response from the API matches some expectation:

  • config: (optional) file path to the kubeconfig to use for this specific test. This allows you to override the defaults.config value from the test scenario.
  • context: (optional) string containing the name of the kube context to use for this specific test. This allows you to override the defaults.context value from the test scenario.
  • namespace: (optional) string containing the name of the Kubernetes namespace to use when performing some action for this specific test. This allows you to override the defaults.namespace value from the test scenario.
  • 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.
  • 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.
  • assert: (optional) object containing assertions to make about the action performed by the test.
  • assert.error: (optional) string to match a returned error from the Kubernetes API server.
  • assert.len: (optional) int with the expected number of items returned.
  • assert.notfound: (optional) bool indicating the test author expects the Kubernetes API to return a 404/Not Found for a resource.
  • assert.unknown: (optional) bool indicating the test author expects the Kubernetes API server to respond that it does not know the type of resource attempting to be fetched or created.
  • assert.matches: (optional) a YAML string, a filepath, or a map[string]interface{} representing the content that you expect to find in the returned result from the kube.get call. If assert.matches is a string, the string can be either a file path to a YAML manifest or inline an YAML string containing the resource fields to compare. Only fields present in the Matches resource are compared. There is a check for existence in the retrieved resource as well as a check that the value of the fields match. Only scalar fields are matched entirely. In other words, you do not need to specify every field of a struct field in order to compare the value of a single field in the nested struct.
  • assert.conditions: (optional) a map, keyed by ConditionType string, of any of the following:
    • a string containing the Status value that the Condition with the ConditionType should have.
    • a list of strings containing the Status value that the Condition with the ConditionType should have.
    • an object containing two fields:
      • status which itself is either a single string or a list of strings containing the Status values that the Condition with the ConditionType should have
      • reason which is the exact string that should be present in the Condition with the ConditionType
  • assert.placement: (optional) an object describing assertions to make about the placement (scheduling outcome) of Pods returned in the kube.get result.
  • assert.placement.spread: (optional) an single string or array of strings for topology keys that the Pods returned in the kube.get result should be spread evenly across, e.g. topology.kubernetes.io/zone or kubernetes.io/hostname.
  • assert.placement.pack: (optional) an single string or array of strings for topology keys that the Pods returned in the kube.get result should be bin-packed within, e.g. topology.kubernetes.io/zone or kubernetes.io/hostname.
  • assert.json: (optional) object describing the assertions to make about resource(s) returned from the kube.get call to the Kubernetes API server.
  • assert.json.len: (optional) integer representing the number of bytes in the resulting JSON object after successfully parsing the resource.
  • assert.json.paths: (optional) map of strings where the keys of the map are JSONPath expressions and the values of the map are the expected value to be found when evaluating the JSONPath expression
  • assert.json.path-formats: (optional) map of strings where the keys of the map are JSONPath expressions and the values of the map are the expected format of the value to be found when evaluating the JSONPath expression. See the list of valid format strings
  • assert.json.schema: (optional) string containing a filepath to a JSONSchema document. If present, the resource's structure will be validated against this JSONSChema document.

Examples

Here are some examples of gdt-kube tests.

Testing that a Pod with the name nginx exists:

name: test-nginx-pod-exists
tests:
 - kube:
     get: pods/nginx
 # These are equivalent. "kube.get" is a shortcut for the longer object.field
 # form above.
 - kube.get: pods/nginx

Testing that a Pod with the name nginx does not exist:

name: test-nginx-pod-not-exist
tests:
 - kube:
     get: pods/nginx
   assert:
     notfound: true

Testing that there are two Pods having the label app:nginx:

name: list-pods-with-labels
tests:
  # You can use the shortcut kube.get
  - name: verify-pods-with-app-nginx-label
    kube.get:
      type: pods
      labels:
        app: nginx
    assert:
      len: 2
  # Or the long-form kube:get
  - name: verify-pods-with-app-nginx-label
    kube:
      get:
        type: pods
        labels:
          app: nginx
    assert:
      len: 2
  # Like "kube.get", you can pass a label selector for "kube.delete"
  - kube.delete:
      type: pods
      labels:
        app: nginx
  # And you can use the long-form kube:delete as well
  - kube:
      delete:
        type: pods
        labels:
          app: nginx

Testing that a Pod with the name nginx exists by the specified timeout (essentially, gdt-kube will retry the get call and assertion until the end of the timeout):

name: test-nginx-pod-exists-within-1-minute
tests:
 - kube.get: pods/nginx
   timeout: 1m

Testing creation and subsequent fetch then delete of a Pod, specifying the Pod definition contained in a YAML file:

name: create-get-delete-pod
description: create, get and delete a Pod
fixtures:
  - kind
tests:
  - name: create-pod
    kube:
      create: manifests/nginx-pod.yaml
  - name: pod-exists
    kube:
      get: pods/nginx
  - name: delete-pod
    kube:
      delete: pods/nginx

Testing creation and subsequent fetch then delete of a Pod, specifying the Pod definition using an inline YAML blob:

name: create-get-delete-pod
description: create, get and delete a Pod
fixtures:
  - kind
tests:
  # "kube.create" is a shortcut for the longer object->field format
  - kube.create: |
        apiVersion: v1
        kind: Pod
        metadata:
          name: nginx
        spec:
          containers:
          - name: nginx
            image: nginx
            imagePullPolicy: IfNotPresent
  # "kube.get" is a shortcut for the longer object->field format
  - kube.get: pods/nginx
  # "kube.delete" is a shortcut for the longer object->field format
  - kube.delete: pods/nginx
Executing arbitrary commands or shell scripts

You can mix other gdt test types in a single gdt test scenario. For example, here we are testing the creation of a Pod, waiting a little while with the wait.after directive, then using the gdt exec test type to test SSH connectivity to the Pod.

name: create-check-ssh
description: create a Deployment then check SSH connectivity
fixtures:
  - kind
tests:
  - kube.create: manifests/deployment.yaml
    wait:
      after: 30s
  - exec: ssh -T someuser@ip
Asserting resource fields using assert.matches

The assert.matches field of a gdt-kube test Spec allows a test author to specify expected fields and those field contents in a resource that was returned by the Kubernetes API server from the result of a kube.get call.

Suppose you have a Deployment resource and you want to write a test that checks that a Deployment resource's Status.ReadyReplicas field is 2.

You do not need to specify all other Deployment.Status fields like Status.Replicas in order to match the Status.ReadyReplicas field value. You only need to include the Status.ReadyReplicas field in the Matches value as these examples demonstrate:

tests:
 - name: check deployment's ready replicas is 2
   kube:
     get: deployments/my-deployment
   assert:
     matches: |
       kind: Deployment
       metadata:
         name: my-deployment
       status:
         readyReplicas: 2

you don't even need to include the kind and metadata in assert.matches. If missing, no kind and name matching will be performed.

tests:
 - name: check deployment's ready replicas is 2
   kube:
     get: deployments/my-deployment
   assert:
     matches: |
       status:
         readyReplicas: 2

In fact, you don't need to use an inline multiline YAML string. You can use a map[string]interface{} as well:

tests:
 - name: check deployment's ready replicas is 2
   kube:
     get: deployments/my-deployment
   assert:
     matches:
       status:
         readyReplicas: 2
Asserting resource Conditions using assert.conditions

assertion.conditions contains the assertions to make about a resource's Status.Conditions collection. It is a map, keyed by the ConditionType (matched case-insensitively), of assertions to make about that Condition. The assertions can be:

  • a string which is the ConditionStatus that should be found for that Condition
  • a list of strings containing ConditionStatuses, any of which should be found for that Condition
  • an object of type ConditionExpect that contains more fine-grained assertions about that Condition's Status and Reason

A simple example that asserts that a Pod's Ready Condition has a status of True. Note that both the condition type ("Ready") and the status ("True") are matched case-insensitively, which means you can just use lowercase strings:

tests:
 - kube:
     get: pods/nginx
   assert:
     conditions:
       ready: true

If we wanted to assert that the ContainersReady Condition had a status of either False or Unknown, we could write the test like this:

tests:
 - kube:
     get: pods/nginx
   assert:
     conditions:
       containersReady:
        - false
        - unknown

Finally, if we wanted to assert that a Deployment's Progressing Condition had a Reason field with a value "NewReplicaSetAvailable" (matched case-sensitively), we could do the following:

tests:
 - kube:
     get: deployments/nginx
   assert:
     conditions:
       progressing:
         status: true
         reason: NewReplicaSetAvailable
Asserting scheduling outcomes using assert.placement

The assert.placement field of a gdt-kube test Spec allows a test author to specify the expected scheduling outcome for a set of Pods returned by the Kubernetes API server from the result of a kube.get call.

Asserting even spread of Pods across a topology

Suppose you have a Deployment resource with a TopologySpreadConstraints that specifies the Pods in the Deployment must land on different hosts:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
       - name: nginx
         image: nginx:latest
         ports:
          - containerPort: 80
      topologySpreadConstraints:
       - maxSkew: 1
         topologyKey: kubernetes.io/hostname
         whenUnsatisfiable: DoNotSchedule
         labelSelector:
           matchLabels:
             app: nginx

You can create a gdt-kube test case that verifies that your nginx Deployment's Pods are evenly spread across all available hosts:

tests:
 - kube:
     get: deployments/nginx
   assert:
     placement:
       spread: kubernetes.io/hostname

If there are more hosts than the spec.replicas in the Deployment, gdt-kube will ensure that each Pod landed on a unique host. If there are fewer hosts than the spec.replicas in the Deployment, gdt-kube will ensure that there is an even spread of Pods to hosts, with any host having no more than one more Pod than any other.

Asserting bin-packing of Pods

Suppose you have configured your Kubernetes scheduler to bin-pack Pods onto hosts by scheduling Pods to hosts with the most allocated CPU resources:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- pluginConfig:
  - args:
      scoringStrategy:
        resources:
        - name: cpu
          weight: 100
        type: MostAllocated
    name: NodeResourcesFit

You can create a gdt-kube test case that verifies that your nginx Deployment's Pods are packed onto the fewest unique hosts:

tests:
 - kube:
     get: deployments/nginx
   assert:
     placement:
       pack: kubernetes.io/hostname

gdt-kube will examine the total number of hosts that meet the nginx Deployment's scheduling and resource constraints and then assert that the number of hosts the Deployment's Pods landed on is the minimum number that would fit the total requested resources.

Asserting resource fields using assert.json

The assert.json field of a gdt-kube test Spec allows a test author to specify expected fields, the value of those fields as well as the format of field values in a resource that was returned by the Kubernetes API server from the result of a kube.get call.

Suppose you have a Deployment resource and you want to write a test that checks that a Deployment resource's Status.ReadyReplicas field is 2.

You can specify this expectation using the assert.json.paths field, which is a map[string]interface{} that takes map keys that are JSONPath expressions and map values of what the field at that JSONPath expression should contain:

tests:
 - name: check deployment's ready replicas is 2
   kube:
     get: deployments/my-deployment
   assert:
     json:
       paths:
         $.status.readyReplicas: 2 

JSONPath expressions can be fairly complex, allowing the test author to, for example, assert the value of a nested map field with a particular key, as this example shows:

tests:
 - name: check deployment's pod template "app" label is "nginx"
   kube:
     get: deployments/my-deployment
   assert:
     json:
       paths:
         $.spec.template.labels["app"]: nginx

You can check that the value of a particular field at a JSONPath is formatted in a particular fashion using assert.json.path-formats. This is a map, keyed by JSONPath expression, of the data format the value of the field at that JSONPath expression should have. Valid data formats are:

  • date
  • date-time
  • email
  • hostname
  • idn-email
  • ipv4
  • ipv6
  • iri
  • iri-reference
  • json-pointer
  • regex
  • relative-json-pointer
  • time
  • uri
  • uri-reference
  • uri-template
  • uuid
  • uuid4

Read more about JSONSchema formats.

For example, suppose we wanted to verify that a Deployment's metadata.uid field was a UUID-4 and that its metadata.creationTimestamp field was a date-time timestamp:

tests:
  - kube:
      get: deployments/nginx
    assert:
      json:
        path-formats:
          $.metadata.uid: uuid4
          $.metadata.creationTimestamp: date-time
Updating a resource and asserting corresponding field changes

Here is an example of creating a Deployment with an initial spec.replicas count of 2, then applying a change to spec.replicas of 1, then asserting that the status.readyReplicas gets updated to 1.

file testdata/manifests/nginx-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

file testdata/apply-deployment.yaml:

name: apply-deployment
description: create, get, apply a change, get, delete a Deployment
fixtures:
  - kind
tests:
  - name: create-deployment
    kube:
      create: testdata/manifests/nginx-deployment.yaml
  - name: deployment-has-2-replicas
    timeout:
      after: 20s
    kube:
      get: deployments/nginx
    assert:
      matches:
        status:
          readyReplicas: 2
  - name: apply-deployment-change
    kube:
      apply: |
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: nginx
        spec:
          replicas: 1
  - name: deployment-has-1-replica
    timeout:
      after: 20s
    kube:
      get: deployments/nginx
    assert:
      matches:
        status:
          readyReplicas: 1
  - name: delete-deployment
    kube:
      delete: deployments/nginx

Determining Kubernetes config, context and namespace values

When evaluating how to construct a Kubernetes client gdt-kube uses the following precedence to determine the kubeconfig and kube context:

  1. The individual test spec's config or context value
  2. Any gdt Fixture that exposes a gdt.kube.config or gdt.kube.context state key (e.g. [KindFixture][kind-fixture]).
  3. The test file's defaults.kube config or context value.

For the kubeconfig file path, if none of the above yielded a value, the following precedence is used to determine the kubeconfig:

  1. A non-empty KUBECONFIG environment variable pointing at a file.
  2. In-cluster config if running in cluster.
  3. $HOME/.kube/config if it exists.

gdt-kube Fixtures

gdt Fixtures are objects that help set up and tear down a testing environment. The gdt-kube library has some utility fixtures to make testing with Kubernetes easier.

KindFixture

The KindFixture eases integration of gdt-kube tests with the KinD local Kubernetes development system.

To use it, import the gdt-kube/fixtures/kind package:

import (
    "github.com/gdt-dev/gdt"
    gdtkube "github.com/gdt-dev/kube"
    gdtkind "github.com/gdt-dev/kube/fixtures/kind"
)

and then register the fixture with your gdt Context, like so:

func TestExample(t *testing.T) {
    s, err := gdt.From("path/to/test.yaml")
    if err != nil {
        t.Fatalf("failed to load tests: %s", err)
    }

    ctx := context.Background()
    ctx = gdt.RegisterFixture(ctx, "kind", gdtkind.New())
    err = s.Run(ctx, t)
    if err != nil {
        t.Fatalf("failed to run tests: %s", err)
    }
}

In your test file, you would list the "kind" fixture in the fixtures list:

name: example-using-kind
fixtures:
 - kind
tests:
 - kube.get: pods/nginx
Retaining and deleting KinD clusters

The default behaviour of the KindFixture is to delete the KinD cluster when the Fixture's Stop() method is called, but only if the KinD cluster did not previously exist before the Fixture's Start() method was called.

If you want to always ensure that a KinD cluster is deleted when the KindFixture is stopped, use the fixtures.kind.WithDeleteOnStop() function:

import (
    "github.com/gdt-dev/gdt"
    gdtkube "github.com/gdt-dev/kube"
    gdtkind "github.com/gdt-dev/kube/fixtures/kind"
)

func TestExample(t *testing.T) {
    s, err := gdt.From("path/to/test.yaml")
    if err != nil {
        t.Fatalf("failed to load tests: %s", err)
    }

    ctx := context.Background()
    ctx = gdt.RegisterFixture(
        ctx, "kind", gdtkind.New(),
        gdtkind.WithDeleteOnStop(),
    )
    err = s.Run(ctx, t)
    if err != nil {
        t.Fatalf("failed to run tests: %s", err)
    }
}

Likewise, the default behaviour of the KindFixture is to retain the KinD cluster when the Fixture's Stop() method is called but only if the KinD cluster previously existed before the Fixture's Start() method was called.

If you want to always ensure a KinD cluster is retained, even if the KindFixture created the KinD cluster, use the fixtures.kind.WithRetainOnStop() function:

import (
    "github.com/gdt-dev/gdt"
    gdtkube "github.com/gdt-dev/kube"
    gdtkind "github.com/gdt-dev/kube/fixtures/kind"
)

func TestExample(t *testing.T) {
    s, err := gdt.From("path/to/test.yaml")
    if err != nil {
        t.Fatalf("failed to load tests: %s", err)
    }

    ctx := context.Background()
    ctx = gdt.RegisterFixture(
        ctx, "kind", gdtkind.New(),
        gdtkind.WithRetainOnStop(),
    )
    err = s.Run(ctx, t)
    if err != nil {
        t.Fatalf("failed to run tests: %s", err)
    }
}
Passing a KinD configuration

You may want to pass a custom KinD configuration resource by using the fixtures.kind.WithConfigPath() modifier:

import (
    "github.com/gdt-dev/gdt"
    gdtkube "github.com/gdt-dev/kube"
    gdtkind "github.com/gdt-dev/kube/fixtures/kind"
)

func TestExample(t *testing.T) {
    s, err := gdt.From("path/to/test.yaml")
    if err != nil {
        t.Fatalf("failed to load tests: %s", err)
    }

    configPath := filepath.Join("testdata", "my-kind-config.yaml")

    ctx := context.Background()
    ctx = gdt.RegisterFixture(
        ctx, "kind", gdtkind.New(),
        gdtkind.WithConfigPath(configPath),
    )
    err = s.Run(ctx, t)
    if err != nil {
        t.Fatalf("failed to run tests: %s", err)
    }
}

Contributing and acknowledgements

gdt was inspired by Gabbi, the excellent Python declarative testing framework. gdt tries to bring the same clear, concise test definitions to the world of Go functional testing.

The Go gopher logo, from which gdt's logo was derived, was created by Renee French.

Contributions to gdt-kube are welcomed! Feel free to open a Github issue or submit a pull request.

Documentation

Index

Constants

View Source
const (
	// StateKeyConfig holds a file path to a kubeconfig
	StateKeyConfig = "kube.config"
	// StateKeyConfigBytes holds a KUBECONFIG object in a bytearray
	StateKeyConfigBytes = "kube.config.bytes"
	// StateKeyContext holds a string kubecontext name
	StateKeyContext = "kube.context"
)

Variables

View Source
var (
	// ErrExpectedMapOrYAMLString is returned when a field that can contain a
	// map[string]interface{} or an embedded YAML string did not contain either
	// of those things.
	// TODO(jaypipes): Move to gdt core?
	ErrExpectedMapOrYAMLString = fmt.Errorf(
		"%w: expected either map[string]interface{} "+
			"or a string with embedded YAML",
		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`",
		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. ",
		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",
		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",
		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",
		api.ErrParse,
	)
	// ErrMatchesInvalid is returned when the `Kube.Assert.Matches` value is
	// malformed.
	ErrMatchesInvalid = fmt.Errorf(
		"%w: `kube.assert.matches` not well-formed",
		api.ErrParse,
	)
	// ErrConditionMatchInvalid is returned when the `Kube.Assert.Conditions`
	// value is malformed.
	ErrConditionMatchInvalid = fmt.Errorf(
		"%w: `kube.assert.conditions` not well-formed",
		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",
		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`",
		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",
		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",
		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",
		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",
		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",
		api.RuntimeError,
	)
)
View Source
var (
	// DefaultTimeout is the default timeout used for each individual test
	// spec. Note that gdt's top-level Scenario.Run handles all timeout and
	// retry behaviour.
	DefaultTimeout = "5s"
)

Functions

func ConditionDoesNotMatch

func ConditionDoesNotMatch(msg string) error

ConditionDoesNotMatch returns ErrConditionDoesNotMatch when a `kube.assert.conditions` object did not match the returned resource.

func ConditionMatchInvalid

func ConditionMatchInvalid(node *yaml.Node, err error) error

ConditionMatchInvalid returns ErrConditionMatchInvalid when a `kube.assert.conditions` field contains invalid YAML content.

func ConnectError added in v1.1.0

func ConnectError(err error) error

ConnectError returns ErrConnnect when an error is found trying to construct a Kubernetes client connection.

func EitherShortcutOrKubeSpecAt added in v1.3.0

func EitherShortcutOrKubeSpecAt(node *yaml.Node) error

EitherShortcutOrKubeSpecAt returns ErrEitherShortcutOrKubeSpec for a given YAML node

func ExpectedMapOrYAMLStringAt added in v1.1.1

func ExpectedMapOrYAMLStringAt(node *yaml.Node) error

ExpectedMapOrYAMLStringAt returns ErrExpectedMapOrYAMLString for a given YAML node

func ExpectedNotFound

func ExpectedNotFound(msg string) error

ExpectedNotFound returns ErrExpectedNotFound for a given status code or number of items.

func InvalidResourceSpecifier

func InvalidResourceSpecifier(subject string, node *yaml.Node) error

InvalidResourceSpecifier returns ErrResourceSpecifier for a given supplied resource specifier.

func InvalidResourceSpecifierOrFilepath

func InvalidResourceSpecifierOrFilepath(
	subject string, node *yaml.Node,
) error

InvalidResourceSpecifierOrFilepath returns ErrResourceSpecifierOrFilepath for a given supplied subject.

func InvalidWithLabels

func InvalidWithLabels(err error, node *yaml.Node) error

InvalidWithLabels returns ErrWithLabels with an error containing more context.

func KubeConfigNotFound

func KubeConfigNotFound(path string) error

KubeConfigNotFound returns ErrKubeConfigNotFound for a given filepath

func MatchesInvalid

func MatchesInvalid(matches interface{}) error

MatchesInvalid returns ErrMatchesInvalid when a `kube.assert.matches` field is not well-formed.

func MatchesInvalidUnmarshalError

func MatchesInvalidUnmarshalError(err error) error

MatchesInvalidUnmarshalError returns ErrMatchesInvalid when a `kube.assert.matches` field contains invalid YAML content.

func MatchesNotEqual

func MatchesNotEqual(msg string) error

MatchesNotEqual returns ErrMatchesNotEqual when a `kube.assert.matches` object did not match the returned resource.

func MoreThanOneKubeActionAt added in v1.3.0

func MoreThanOneKubeActionAt(node *yaml.Node) error

MoreThanOneKubeActionAt returns ErrMoreThanOneKubeAction for a given YAML node

func Plugin

func Plugin() api.Plugin

Plugin returns the Kubernetes gdt plugin

func ResourceUnknown

func ResourceUnknown(arg string) error

ResourceUnknown returns ErrRuntimeResourceUnknown for a given resource or kind arg string

Types

type Action added in v1.3.0

type Action struct {
	// Create is a string containing a file path or raw YAML content describing
	// a Kubernetes resource to call `kubectl create` with.
	Create string `yaml:"create,omitempty"`
	// Apply is a string containing a file path or raw YAML content describing
	// a Kubernetes resource to call `kubectl apply` with.
	Apply string `yaml:"apply,omitempty"`
	// Delete is a string or object containing arguments to `kubectl delete`.
	//
	// It must be one of the following:
	//
	// - a file path to a manifest that will be read and the resources
	//   described in the manifest will be deleted
	// - a resource kind or kind alias, e.g. "pods", "po", followed by one of
	//   the following:
	//   * a space or `/` character followed by the resource name to delete
	//     only a resource with that name.
	// - an object with a `type` and optional `labels` field containing a label
	//   selector that should be used to select that `type` of resource.
	Delete *ResourceIdentifierOrFile `yaml:"delete,omitempty"`
	// Get is a string or object containing arguments to `kubectl get`.
	//
	// It must be one of the following:
	//
	// - a string with a resource kind or kind alias, e.g. "pods", "po",
	//   followed by one of the following:
	//   * a space or `/` character followed by the resource name to get only a
	//     resource with that name.
	// - an object with a `type` and optional `labels` field containing a label
	//   selector that should be used to select that `type` of resource.
	Get *ResourceIdentifier `yaml:"get,omitempty"`
}

Action describes the the Kubernetes-specific action that is performed by the test.

func (*Action) Do added in v1.3.0

func (a *Action) Do(
	ctx context.Context,
	c *connection,
	ns string,
	out *interface{},
) error

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 (*Action) UnmarshalYAML added in v1.3.0

func (a *Action) UnmarshalYAML(node *yaml.Node) error

type ConditionMatch

type ConditionMatch struct {
	// contains filtered or unexported fields
}

ConditionMatch can be a string (the ConditionStatus to match), a slice of strings (any of the ConditionStatus values to match) or an object with Status and Reason fields describing the Condition fields we want to match on.

func (*ConditionMatch) UnmarshalYAML

func (m *ConditionMatch) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML is a custom unmarshaler that understands that the value of the ConditionMatch can be either a string, a slice of strings, or an object with .

type Defaults

type Defaults struct {
	// contains filtered or unexported fields
}

Defaults is the known HTTP plugin defaults collection

func (*Defaults) UnmarshalYAML

func (d *Defaults) UnmarshalYAML(node *yaml.Node) error

type Expect

type Expect struct {
	// Error is a string that is expected to be returned as an error string
	// from the client call
	// TODO(jaypipes): Make this polymorphic to be either a shortcut string
	// (like this) or a struct containing individual error assertion fields.
	Error string `yaml:"error,omitempty"`
	// Len is an integer that is expected to represent the number of items in
	// the response when the Get request was translated into a List operation
	// (i.e. when the resource specified was a plural kind
	Len *int `yaml:"len,omitempty"`
	// NotFound is a bool indicating the result of a call should be a
	// NotFound error. Alternately, the user can set `assert.len = 0` and for
	// single-object-returning calls (e.g. `get` or `delete`) the assertion is
	// equivalent to `assert.notfound = true`
	NotFound bool `yaml:"notfound,omitempty"`
	// Unknown is a bool indicating the test author expects that they will have
	// gotten an error ("the server could not find the requested resource")
	// from the Kubernetes API server. This is mostly good for unit/fuzz
	// testing CRDs.
	Unknown bool `yaml:"unknown,omitempty"`
	// Matches is either a string or a map[string]interface{} containing the
	// resource that the `Kube.Get` should match against. If Matches is a
	// string, the string can be either a file path to a YAML manifest or
	// inline an YAML string containing the resource fields to compare.
	//
	// Only fields present in the Matches resource are compared. There is a
	// check for existence in the retrieved resource as well as a check that
	// the value of the fields match. Only scalar fields are matched entirely.
	// In other words, you do not need to specify every field of a struct field
	// in order to compare the value of a single field in the nested struct.
	//
	// As an example, imagine you wanted to check that a Deployment resource's
	// `Status.ReadyReplicas` field was 2. You do not need to specify all other
	// `Deployment.Status` fields like `Status.Replicas` in order to match the
	// `Status.ReadyReplicas` field value. You only need to include the
	// `Status.ReadyReplicas` field in the `Matches` value as these examples
	// demonstrate:
	//
	// “`yaml
	// tests:
	//  - name: check deployment's ready replicas is 2
	//    kube:
	//      get: deployments/my-deployment
	//      assert:
	//        matches: |
	//          kind: Deployment
	//          metadata:
	//            name: my-deployment
	//          status:
	//            readyReplicas: 2
	// “`
	//
	// you don't even need to include the kind and metadata in `Matches`. If
	// missing, no kind and name matching will be performed.
	//
	// “`yaml
	// tests:
	//  - name: check deployment's ready replicas is 2
	//    kube:
	//      get: deployments/my-deployment
	//      assert:
	//        matches: |
	//          status:
	//            readyReplicas: 2
	// “`
	//
	// In fact, you don't need to use an inline multiline YAML string. You can
	// use a `map[string]interface{}` as well:
	//
	// “`yaml
	// tests:
	//  - name: check deployment's ready replicas is 2
	//    kube:
	//      get: deployments/my-deployment
	//      assert:
	//        matches:
	//          status:
	//            readyReplicas: 2
	// “`
	Matches interface{} `yaml:"matches,omitempty"`
	// JSON contains the assertions about JSON data in a response from the
	// Kubernetes API server.
	JSON *gdtjson.Expect `yaml:"json,omitempty"`
	// Conditions contains the assertions to make about a resource's
	// `Status.Conditions` collection. It is a map, keyed by the ConditionType
	// (matched case-insensitively), of assertions to make about that
	// Condition. The assertions can be:
	//
	// * a string which is the ConditionStatus that should be found for that
	//   Condition
	// * a list of strings containing ConditionStatuses, any of which should be
	//   found for that Condition.
	// * an object of type `ConditionExpect` that contains more fine-grained
	//   assertions about that Condition.
	//
	// A simple example that asserts that a Pod's `Ready` Condition has a
	// status of `True`. Note that both the condition type ("Ready") and the
	// status ("True") are matched case-insensitively, which means you can just
	// use lowercase strings:
	//
	// “`yaml
	// tests:
	//  - kube:
	//      get: pods/nginx
	//      assert:
	//        conditions:
	//          ready: true
	// “`
	//
	// If we wanted to assert that the `ContainersReady` Condition had a status
	// of either `False` or `Unknown`, we could write the test like this:
	//
	// “`yaml
	// tests:
	//  - kube:
	//      get: pods/nginx
	//      assert:
	//        conditions:
	//          containersReady:
	//           - false
	//           - unknown
	// “`
	//
	// Finally, if we wanted to assert that a Deployment's `Progressing`
	// Condition had a Reason field with a value "NewReplicaSetAvailable"
	// (matched case-sensitively), we could do the following:
	//
	// “`yaml
	// tests:
	//  - kube:
	//      get: deployments/nginx
	//      assert:
	//        conditions:
	//          progressing:
	//            status: true
	//            reason: NewReplicaSetAvailable
	// “`
	Conditions map[string]*ConditionMatch `yaml:"conditions,omitempty"`
	// Placement describes expected Pod scheduling spread or pack outcomes.
	Placement *PlacementAssertion `yaml:"placement,omitempty"`
}

Expect contains one or more assertions about a kube client call

func (*Expect) UnmarshalYAML added in v1.1.1

func (e *Expect) UnmarshalYAML(node *yaml.Node) error

type KubeSpec

type KubeSpec struct {
	Action
	// Config is the path of the kubeconfig to use in executing Kubernetes
	// client calls for this Spec. If empty, the `kube` defaults' `config`
	// value will be used. If that is empty, the following precedence is used:
	//
	// 1) KUBECONFIG environment variable pointing at a file.
	// 2) In-cluster config if running in cluster.
	// 3) $HOME/.kube/config if exists.
	Config string `yaml:"config,omitempty"`
	// Context is the name of the kubecontext to use for this Spec. If empty,
	// the `kube` defaults' `context` value will be used. If that is empty, the
	// kubecontext marked default in the kubeconfig is used.
	Context string `yaml:"context,omitempty"`
	// Namespace is a string indicating the Kubernetes namespace to use when
	// calling the Kubernetes API. If empty, any namespace specified in the
	// Defaults is used and then the string "default" is used.
	Namespace string `yaml:"namespace,omitempty"`
}

KubeSpec is the complex type containing all of the Kubernetes-specific actions. Most users will use the `kube.create`, `kube.apply` and `kube.describe` shortcut fields.

func (*KubeSpec) UnmarshalYAML added in v1.3.0

func (s *KubeSpec) UnmarshalYAML(node *yaml.Node) error

type PlacementAssertion added in v1.4.0

type PlacementAssertion struct {
	// Spread contains zero or more topology keys that gdt-kube will assert an
	// even spread across.
	Spread *api.FlexStrings `yaml:"spread,omitempty"`
	// Pack contains zero or more topology keys that gdt-kube will assert
	// bin-packing of resources within.
	Pack *api.FlexStrings `yaml:"pack,omitempty"`
}

PlacementAssertion describes an expectation for Pod scheduling outcomes.

type ResourceIdentifier added in v1.3.0

type ResourceIdentifier struct {
	Arg    string            `yaml:"-"`
	Name   string            `yaml:"-"`
	Labels map[string]string `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.

func NewResourceIdentifier added in v1.3.0

func NewResourceIdentifier(
	arg string,
	name string,
	labels map[string]string,
) *ResourceIdentifier

func (*ResourceIdentifier) Title added in v1.3.0

func (r *ResourceIdentifier) Title() string

Title returns the resource identifier's kind and name, if present

func (*ResourceIdentifier) UnmarshalYAML added in v1.3.0

func (r *ResourceIdentifier) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML is a custom unmarshaler that understands that the value of the ResourceIdentifier can be either a string or a selector.

type ResourceIdentifierOrFile added in v1.3.0

type ResourceIdentifierOrFile struct {
	Arg    string            `yaml:"-"`
	Name   string            `yaml:"-"`
	Labels map[string]string `yaml:"-"`
	// contains filtered or unexported fields
}

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.

func NewResourceIdentifierOrFile added in v1.3.0

func NewResourceIdentifierOrFile(
	fp string,
	arg string,
	name string,
	labels map[string]string,
) *ResourceIdentifierOrFile

func (*ResourceIdentifierOrFile) FilePath added in v1.3.0

func (r *ResourceIdentifierOrFile) FilePath() string

FilePath returns the resource identifier's file path, if present

func (*ResourceIdentifierOrFile) Title added in v1.3.0

func (r *ResourceIdentifierOrFile) Title() string

Title returns the resource identifier's file name, if present, or the kind and name, if present

func (*ResourceIdentifierOrFile) UnmarshalYAML added in v1.3.0

func (r *ResourceIdentifierOrFile) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML is a custom unmarshaler that understands that the value of the ResourceIdentifierOrFile can be either a string or a selector.

type Spec

type Spec struct {
	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.
	Kube *KubeSpec `yaml:"kube,omitempty"`
	// KubeCreate is a shortcut for the `KubeSpec.Create`. It can contain
	// either a file path or raw YAML content describing a Kubernetes resource
	// to call `kubectl create` with.
	KubeCreate string `yaml:"kube.create,omitempty"`
	// KubeGet is a string containing an argument to `kubectl get` and must be
	// one of the following:
	//
	// - a file path to a manifest that will be read and the resources within
	//   retrieved via `kubectl get`
	// - a resource kind or kind alias, e.g. "pods", "po", followed by one of
	//   the following:
	//   * a space or `/` character followed by the resource name to get only a
	//     resource with that name.
	//   * a space followed by `-l ` followed by a label to get resources
	//     having such a label.
	KubeGet string `yaml:"kube.get,omitempty"`
	// KubeApply is a shortcut for the `KubeSpec.Apply`. It is a string
	// containing a file path or raw YAML content describing a Kubernetes
	// resource to call `kubectl apply` with.
	KubeApply string `yaml:"kube.apply,omitempty"`
	// KubeDelete is a shortcut for the `KubeSpec.Delete`. It is a string
	// containing an argument to `kubectl delete` and must be one of the
	// following:
	//
	// - a file path to a manifest that will be read and the resources
	//   described in the manifest will be deleted
	// - a resource kind or kind alias, e.g. "pods", "po", followed by one of
	//   the following:
	//   * a space or `/` character followed by the resource name to delete
	//     only a resource with that name.
	//   * a space followed by `-l ` followed by a label to delete resources
	//     having such a label.
	//   * the string `--all` to delete all resources of that kind.
	KubeDelete string `yaml:"kube.delete,omitempty"`
	// Assert houses the various assertions to be made about the kube client
	// call (Create, Apply, Get, etc)
	// TODO(jaypipes): Make this polymorphic to be either a single assertion
	// struct or a list of assertion structs
	Assert *Expect `yaml:"assert,omitempty"`
}

Spec describes a test of a *single* Kubernetes API request and response.

func (*Spec) Base

func (s *Spec) Base() *api.Spec

func (*Spec) Config

func (s *Spec) Config(ctx context.Context) (*rest.Config, error)

Config returns a Kubernetes client-go rest.Config to use for this Spec. We evaluate where to retrieve the Kubernetes config from by looking at the following things, in this order:

1) The Spec.Kube.Config value 2) Any Fixtures that return a `kube.config` or `kube.config.bytes` state key 3) The Defaults.Config value 4) KUBECONFIG environment variable pointing at a file. 5) In-cluster config if running in cluster. 6) $HOME/.kube/config if exists.

func (*Spec) Eval added in v1.1.0

func (s *Spec) Eval(ctx context.Context) (*api.Result, error)

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 (*Spec) Namespace

func (s *Spec) Namespace() string

Namespace returns the Kubernetes namespace to use when calling the Kubernetes API server. We evaluate which namespace to use by looking at the following things, in this order:

1) The Spec.Kube.Namespace value 2) The Defaults.Namespace value 3) Use the string "default"

func (*Spec) Retry added in v1.9.0

func (s *Spec) Retry() *api.Retry

func (*Spec) SetBase

func (s *Spec) SetBase(b api.Spec)

func (*Spec) Timeout added in v1.9.0

func (s *Spec) Timeout() *api.Timeout

func (*Spec) Title

func (s *Spec) Title() string

Title returns a good name for the Spec

func (*Spec) UnmarshalYAML

func (s *Spec) UnmarshalYAML(node *yaml.Node) error

Directories

Path Synopsis
fixtures

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL