Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Go Declarative Testing - Kubernetes

License

NotificationsYou must be signed in to change notification settings

gdt-dev/kube

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Go ReferenceGo Report CardBuild StatusContributor Covenant

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

Thisgithub.com/gdt-dev/kube (shortened hereafter togdt-kube) repositoryis a companion Go library forgdt that allows test authors to cleanlydescribe functional tests of Kubernetes resources and actions using a simple,clear YAML format.gdt-kube parses YAML files that describe Kubernetesclient/API requests and assertions about those client calls.

Usage

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

Import thegdt andgdt-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 thegdt.From() function to instantiate atest object (either aScenario or aSuite) that can beRun() with astandard Gocontext.Context and a standard Go*testing.T type:

funcTestExample(t*testing.T) {s,err:=gdt.From("path/to/test.yaml")iferr!=nil {t.Fatalf("failed to load tests: %s",err)    }ctx:=context.Background()err=s.Run(ctx,t)iferr!=nil {t.Fatalf("failed to run tests: %s",err)    }}

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

gdt is adeclarative testing framework and the meat of your tests is goingto be in the YAML files that describe the actions and assertions for one ormore tests. Read on for an explanation of how to write tests in thisdeclarative YAML format.

gdt-kube test file structure

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

Allgdt scenarios have the following fields:

  • name: (optional) string describing the contents of the test file. Ifmissing or empty, the filename is used as the name
  • description: (optional) string with longer description of the test filecontents
  • defaults: (optional) is a map, keyed by a plugin name, of default optionsand configuration values for that plugin.
  • fixtures: (optional) list of strings indicating named fixtures that will bestarted before any of the tests in the file are run
  • tests: list ofSpec specializations that represent therunnable test units in the test scenario.

gdt-kube test configuration defaults

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

  • defaults.kube.config: (optional) file path to akubeconfig to use for thetest scenario.
  • defaults.kube.context: (optional) string containing the name of the kubecontext to use for the test scenario.
  • defaults.kube.namespace: (optional) string containing the Kubernetesnamespace to use when performing some action for the test scenario.

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

name:example-test-with-defaultsdefaults:kube:context:my-kube-contextnamespace:my-namespace

gdt-kube test spec structure

Allgdt 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 testunit.
  • timeout: (optional) a string duration of time the test unit is expected tocomplete within.
  • retry: (optional) an object containing retry configurationu for the testunit. Some plugins will automatically attempt to retry the test action whenan assertion fails. This field allows you to control this retry behaviour foreach individual test.
  • retry.interval: (optional) a string duration of time that the test pluginwill retry the test action in the event assertions fail. The default intervalfor retries is plugin-dependent.
  • retry.attempts: (optional) an integer indicating the number of times that aplugin will retry the test action in the event assertions fail. The defaultnumber of attempts for retries is plugin-dependent.
  • retry.exponential: (optional) a boolean indicating an exponential backoffshould be applied to the retry interval. The default is is plugin-dependent.
  • wait (optional) an object containingwait information for the testunit.
  • wait.before: a string duration of time that gdt should wait beforeexecuting the test unit's action.
  • wait.after: a string duration of time that gdt should wait after executingthe test unit's action.

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

  • config: (optional) file path to thekubeconfig to use for this specifictest. This allows you to override thedefaults.config value from the testscenario.
  • context: (optional) string containing the name of the kube context to usefor this specific test. This allows you to override thedefaults.contextvalue from the test scenario.
  • namespace: (optional) string containing the name of the Kubernetesnamespace to use when performing some action for this specific test. Thisallows you to override thedefaults.namespace value from the test scenario.
  • kube: (optional) an object containing actions and assertions the test takesagainst 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 readfrom the Kubernetes API server.
  • kube.create: (optional) string containing either a file path to a YAMLmanifest or a string of raw YAML containing the resource(s) to create.
  • kube.apply: (optional) string containing either a file path to a YAMLmanifest or a string of raw YAML containing the resource(s) for whichgdt-kube will perform a Kubernetes Apply call.
  • kube.delete: (optional) string or object containing either a resourceidentifier (e.g.pods,po/nginx , a file path to a YAML manifest, or alabel selector for resources that will be deleted.
  • assert: (optional) object containing assertions to make about theaction performed by the test.
  • assert.error: (optional) string to match a returned error from theKubernetes API server.
  • assert.len: (optional) int with the expected number of items returned.
  • assert.notfound: (optional) bool indicating the test author expectsthe Kubernetes API to return a 404/Not Found for a resource.
  • assert.unknown: (optional) bool indicating the test author expects theKubernetes API server to respond that it does not know the type of resourceattempting to be fetched or created.
  • assert.matches: (optional) a YAML string, a filepath, or amap[string]interface{} representing the content that you expect to find inthe returned result from thekube.get call. Ifassert.matches is astring, the string can be either a file path to a YAML manifest orinline an YAML string containing the resource fields to compare.Only fields present in the Matches resource are compared. There is acheck for existence in the retrieved resource as well as a check thatthe 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 fieldin order to compare the value of a single field in the nested struct.
  • assert.conditions: (optional) a map, keyed byConditionType string,of any of the following:
    • a string containing theStatus value that theCondition with theConditionType should have.
    • a list of strings containing theStatus value that theCondition withtheConditionType should have.
    • an object containing two fields:
      • status which itself is either a single string or a list of stringscontaining theStatus values that theCondition with theConditionType should have
      • reason which is the exact string that should be present in theCondition with theConditionType
  • assert.placement: (optional) an object describing assertions to make aboutthe placement (scheduling outcome) of Pods returned in thekube.get result.
  • assert.placement.spread: (optional) an single string or array of stringsfor topology keys that the Pods returned in thekube.get result should bespread evenly across, e.g.topology.kubernetes.io/zone orkubernetes.io/hostname.
  • assert.placement.pack: (optional) an single string or array of strings fortopology keys that the Pods returned in thekube.get result should bebin-packed within, e.g.topology.kubernetes.io/zone orkubernetes.io/hostname.
  • assert.json: (optional) object describing the assertions to make aboutresource(s) returned from thekube.get call to the Kubernetes API server.
  • assert.json.len: (optional) integer representing the number of bytes in theresulting JSON object after successfully parsing the resource.
  • assert.json.paths: (optional) map of strings where the keys of the mapare JSONPath expressions and the values of the map are the expected value tobe found when evaluating the JSONPath expression
  • assert.json.path-formats: (optional) map of strings where the keys of the map areJSONPath expressions and the values of the map are the expected format of thevalue to be found when evaluating the JSONPath expression. See thelist of valid format strings
  • assert.json.schema: (optional) string containing a filepath to aJSONSchema document. If present, the resource's structure will be validatedagainst this JSONSChema document.

Examples

Here are some examples ofgdt-kube tests.

Testing that a Pod with the namenginx exists:

name:test-nginx-pod-existstests: -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 namenginxdoes not exist:

name:test-nginx-pod-not-existtests: -kube:get:pods/nginxassert:notfound:true

Testing that there are two Pods having the labelapp:nginx:

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

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

name:test-nginx-pod-exists-within-1-minutetests: -kube.get:pods/nginxtimeout:1m

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

name:create-get-delete-poddescription:create, get and delete a Podfixtures:  -kindtests:  -name:create-podkube:create:manifests/nginx-pod.yaml  -name:pod-existskube:get:pods/nginx  -name:delete-podkube:delete:pods/nginx

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

name:create-get-delete-poddescription:create, get and delete a Podfixtures:  -kindtests:# "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 othergdt test types in a singlegdt test scenario. Forexample, here we are testing the creation of a Pod, waiting a little while withthewait.after directive, then using thegdtexec test type to test SSHconnectivity to the Pod.

name:create-check-sshdescription:create a Deployment then check SSH connectivityfixtures:  -kindtests:  -kube.create:manifests/deployment.yamlwait:after:30s  -exec:ssh -T someuser@ip

Asserting resource fields usingassert.matches

Theassert.matches field of agdt-kube test Spec allows a test authorto specify expected fields and those field contents in a resource that wasreturned by the Kubernetes API server from the result of akube.get call.

Suppose you have a Deployment resource and you want to write a test that checksthat a Deployment resource'sStatus.ReadyReplicas field is2.

You do not need to specify all otherDeployment.Status fields likeStatus.Replicas in order to match theStatus.ReadyReplicas field value. Youonly need to include theStatus.ReadyReplicas field in theMatches value asthese examples demonstrate:

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

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

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

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

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

Asserting resourceConditions usingassert.conditions

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

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

A simple example that asserts that a Pod'sReady Condition has astatus ofTrue. Note that both the condition type ("Ready") and thestatus ("True") are matched case-insensitively, which means you can justuse lowercase strings:

tests: -kube:get:pods/nginxassert:conditions:ready:true

If we wanted to assert that theContainersReady Condition had a statusof eitherFalse orUnknown, we could write the test like this:

tests: -kube:get:pods/nginxassert:conditions:containersReady:        -false        -unknown

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

tests: -kube:get:deployments/nginxassert:conditions:progressing:status:truereason:NewReplicaSetAvailable

Asserting scheduling outcomes usingassert.placement

Theassert.placement field of agdt-kube test Spec allows a test author tospecify the expected scheduling outcome for a set of Pods returned by theKubernetes API server from the result of akube.get call.

Asserting even spread of Pods across a topology

Suppose you have a Deployment resource with aTopologySpreadConstraints thatspecifies the Pods in the Deployment must land on different hosts:

apiVersion:apps/v1kind:Deploymentmetadata:name:nginx-deploymentlabels:app:nginxspec:replicas:3selector:matchLabels:app:nginxtemplate:metadata:labels:app:nginxspec:containers:       -name:nginximage:nginx:latestports:          -containerPort:80topologySpreadConstraints:       -maxSkew:1topologyKey:kubernetes.io/hostnamewhenUnsatisfiable:DoNotSchedulelabelSelector:matchLabels:app:nginx

You can create agdt-kube test case that verifies that yournginxDeployment's Pods are evenly spread across all available hosts:

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

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

Asserting bin-packing of Pods

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

apiVersion:kubescheduler.config.k8s.io/v1kind:KubeSchedulerConfigurationprofiles:-pluginConfig:  -args:scoringStrategy:resources:        -name:cpuweight:100type:MostAllocatedname:NodeResourcesFit

You can create agdt-kube test case that verifies that yournginxDeployment's Pods are packed onto the fewest unique hosts:

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

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

Asserting resource fields usingassert.json

Theassert.json field of agdt-kube test Spec allows a test author tospecify expected fields, the value of those fields as well as the format offield values in a resource that was returned by the Kubernetes API server fromthe result of akube.get call.

Suppose you have a Deployment resource and you want to write a test that checksthat a Deployment resource'sStatus.ReadyReplicas field is2.

You can specify this expectation using theassert.json.paths field,which is amap[string]interface{} that takes map keys that are JSONPathexpressions and map values of what the field at that JSONPath expression shouldcontain:

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

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

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

You can check that the value of a particular field at a JSONPath is formattedin a particular fashion usingassert.json.path-formats. This is a map,keyed by JSONPath expression, of the data format the value of the field at thatJSONPath 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'smetadata.uidfield was a UUID-4 and that itsmetadata.creationTimestamp field was adate-time timestamp:

tests:  -kube:get:deployments/nginxassert: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 initialspec.replicascount of 2, then applying a change tospec.replicas of 1, then asserting thatthestatus.readyReplicas gets updated to 1.

filetestdata/manifests/nginx-deployment.yaml:

apiVersion:apps/v1kind:Deploymentmetadata:name:nginxspec:selector:matchLabels:app:nginxreplicas:2template:metadata:labels:app:nginxspec:containers:      -name:nginximage:nginxports:        -containerPort:80

filetestdata/apply-deployment.yaml:

name:apply-deploymentdescription:create, get, apply a change, get, delete a Deploymentfixtures:  -kindtests:  -name:create-deploymentkube:create:testdata/manifests/nginx-deployment.yaml  -name:deployment-has-2-replicastimeout:after:20skube:get:deployments/nginxassert:matches:status:readyReplicas:2  -name:apply-deployment-changekube:apply:|        apiVersion: apps/v1        kind: Deployment        metadata:          name: nginx        spec:          replicas: 1  -name:deployment-has-1-replicatimeout:after:20skube:get:deployments/nginxassert:matches:status:readyReplicas:1  -name:delete-deploymentkube:delete:deployments/nginx

Determining Kubernetes config, context and namespace values

When evaluating how to construct a Kubernetes clientgdt-kube uses the followingprecedence to determine thekubeconfig and kube context:

  1. The individual test spec'sconfig orcontext value
  2. Anygdt Fixture that exposes agdt.kube.config orgdt.kube.contextstate key (e.g. [KindFixture][kind-fixture]).
  3. The test file'sdefaults.kubeconfig orcontext value.

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

  1. A non-emptyKUBECONFIG 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 testingenvironment. Thegdt-kube library has some utility fixtures to make testingwith Kubernetes easier.

KindFixture

TheKindFixture eases integration ofgdt-kube tests with the KinD localKubernetes development system.

To use it, import thegdt-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 yourgdtContext, like so:

funcTestExample(t*testing.T) {s,err:=gdt.From("path/to/test.yaml")iferr!=nil {t.Fatalf("failed to load tests: %s",err)    }ctx:=context.Background()ctx=gdt.RegisterFixture(ctx,"kind",gdtkind.New())err=s.Run(ctx,t)iferr!=nil {t.Fatalf("failed to run tests: %s",err)    }}

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

name:example-using-kindfixtures: -kindtests: -kube.get:pods/nginx

Retaining and deleting KinD clusters

The default behaviour of theKindFixture is to delete the KinD cluster whenthe Fixture'sStop() method is called, butonly if the KinD cluster did notpreviously exist before the Fixture'sStart() method was called.

If you want toalways ensure that a KinD cluster is deleted when theKindFixture is stopped, use thefixtures.kind.WithDeleteOnStop() function:

import ("github.com/gdt-dev/gdt"    gdtkube"github.com/gdt-dev/kube"    gdtkind"github.com/gdt-dev/kube/fixtures/kind")funcTestExample(t*testing.T) {s,err:=gdt.From("path/to/test.yaml")iferr!=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)iferr!=nil {t.Fatalf("failed to run tests: %s",err)    }}

Likewise, the default behaviour of theKindFixture is to retain the KinDcluster when the Fixture'sStop() method is called butonly if the KinDcluster previously existed before the Fixture'sStart() method was called.

If you want toalways ensure a KinD cluster is retained, even if theKindFixture created the KinD cluster, use thefixtures.kind.WithRetainOnStop() function:

import ("github.com/gdt-dev/gdt"    gdtkube"github.com/gdt-dev/kube"    gdtkind"github.com/gdt-dev/kube/fixtures/kind")funcTestExample(t*testing.T) {s,err:=gdt.From("path/to/test.yaml")iferr!=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)iferr!=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 thefixtures.kind.WithConfigPath() modifier:

import ("github.com/gdt-dev/gdt"    gdtkube"github.com/gdt-dev/kube"    gdtkind"github.com/gdt-dev/kube/fixtures/kind")funcTestExample(t*testing.T) {s,err:=gdt.From("path/to/test.yaml")iferr!=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)iferr!=nil {t.Fatalf("failed to run tests: %s",err)    }}

Contributing and acknowledgements

gdt was inspired byGabbi, the excellentPython 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 ReneeFrench.

Contributions togdt-kube are welcomed! Feel free to open a Github issue orsubmit a pull request.


[8]ページ先頭

©2009-2025 Movatter.jp