{"id":13643768,"url":"https://github.com/gdt-dev/kube","last_synced_at":"2026-03-10T09:47:17.849Z","repository":{"id":183805381,"uuid":"670712691","full_name":"gdt-dev/kube","owner":"gdt-dev","description":"Go Declarative Testing - Kubernetes","archived":false,"fork":false,"pushed_at":"2025-12-30T17:02:21.000Z","size":295,"stargazers_count":7,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-01-03T12:30:45.882Z","etag":null,"topics":["declarative-testing","go","go-testing","golang-testing","kubernetes","kubernetes-testing"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gdt-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"COPYING","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-07-25T16:57:09.000Z","updated_at":"2025-12-30T17:01:54.000Z","dependencies_parsed_at":"2024-01-15T17:33:32.412Z","dependency_job_id":"67fa9c9f-97bb-43dc-b46c-79b8165ebdca","html_url":"https://github.com/gdt-dev/kube","commit_stats":null,"previous_names":["gdt-dev/kube"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/gdt-dev/kube","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdt-dev%2Fkube","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdt-dev%2Fkube/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdt-dev%2Fkube/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdt-dev%2Fkube/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gdt-dev","download_url":"https://codeload.github.com/gdt-dev/kube/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdt-dev%2Fkube/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30329164,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T05:25:20.737Z","status":"ssl_error","status_checked_at":"2026-03-10T05:25:17.430Z","response_time":106,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["declarative-testing","go","go-testing","golang-testing","kubernetes","kubernetes-testing"],"created_at":"2024-08-02T01:01:52.344Z","updated_at":"2026-03-10T09:47:17.828Z","avatar_url":"https://github.com/gdt-dev.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# Go Declarative Testing - Kubernetes\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/gdt-dev/kube.svg)](https://pkg.go.dev/github.com/gdt-dev/kube)\n[![Go Report Card](https://goreportcard.com/badge/github.com/gdt-dev/kube)](https://goreportcard.com/report/github.com/gdt-dev/kube)\n[![Build Status](https://github.com/gdt-dev/kube/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/gdt-dev/kube/actions)\n[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)\n\n\u003cdiv style=\"float: left\"\u003e\n\u003cimg align=left src=\"static/gdtkubelogo400x544.png\" width=200px /\u003e\n\u003c/div\u003e\n\n[`gdt`][gdt] is a testing library that allows test authors to cleanly describe tests\nin a YAML file. `gdt` reads YAML files that describe a test's assertions and\nthen builds a set of Go structures that the standard Go\n[`testing`](https://golang.org/pkg/testing/) package can execute.\n\n[gdt]: https://github.com/gdt-dev/gdt\n\nThis `github.com/gdt-dev/kube` (shortened hereafter to `gdt-kube`) repository\nis a companion Go library for `gdt` that allows test authors to cleanly\ndescribe functional tests of Kubernetes resources and actions using a simple,\nclear YAML format. `gdt-kube` parses YAML files that describe Kubernetes\nclient/API requests and assertions about those client calls.\n\n## Usage\n\n`gdt-kube` is a Go library and is intended to be included in your own Go\napplication's test code as a Go package dependency.\n\nImport the `gdt` and `gdt-kube` libraries in a Go test file:\n\n```go\nimport (\n    \"github.com/gdt-dev/gdt\"\n    gdtkube \"github.com/gdt-dev/kube\"\n)\n```\n\nIn a standard Go test function, use the `gdt.From()` function to instantiate a\ntest object (either a `Scenario` or a `Suite`) that can be `Run()` with a\nstandard Go `context.Context` and a standard Go `*testing.T` type:\n\n```go\nfunc TestExample(t *testing.T) {\n    s, err := gdt.From(\"path/to/test.yaml\")\n    if err != nil {\n        t.Fatalf(\"failed to load tests: %s\", err)\n    }\n\n    ctx := context.Background()\n    err = s.Run(ctx, t)\n    if err != nil {\n        t.Fatalf(\"failed to run tests: %s\", err)\n    }\n}\n```\n\nTo execute the tests, just run `go test` per the standard Go testing practice.\n\n`gdt` is a *declarative testing framework* and the meat of your tests is going\nto be in the YAML files that describe the actions and assertions for one or\nmore tests. Read on for an explanation of how to write tests in this\ndeclarative YAML format.\n\n## `gdt-kube` test file structure\n\nA `gdt` test scenario (or just \"scenario\") is simply a YAML file.\n\nAll `gdt` scenarios have the following fields:\n\n* `name`: (optional) string describing the contents of the test file. If\n  missing or empty, the filename is used as the name\n* `description`: (optional) string with longer description of the test file\n  contents\n* `defaults`: (optional) is a map, keyed by a plugin name, of default options\n  and configuration values for that plugin.\n* `fixtures`: (optional) list of strings indicating named fixtures that will be\n  started before any of the tests in the file are run\n* `tests`: list of [`Spec`][basespec] specializations that represent the\n  runnable test units in the test scenario.\n\n[basespec]: https://github.com/gdt-dev/core/blob/023f92ee3468852d1d477df91cf42789e472b3b5/api/spec.go#L27-L48\n\n### `gdt-kube` test configuration defaults\n\nTo set `gdt-kube`-specific default configuration values for the test scenario,\nset the `defaults.kube` field to an object containing any of these fields:\n\n* `defaults.kube.config`: (optional) file path to a `kubeconfig` to use for the\n  test scenario.\n* `defaults.kube.context`: (optional) string containing the name of the kube\n  context to use for the test scenario.\n* `defaults.kube.namespace`: (optional) string containing the Kubernetes\n  namespace to use when performing some action for the test scenario.\n\nAs an example, let's say that I wanted to override the Kubernetes namespace and\nthe kube context used for a particular test scenario. I would do the following:\n\n```yaml\nname: example-test-with-defaults\ndefaults:\n  kube:\n    context: my-kube-context\n    namespace: my-namespace\n```\n\n### `gdt-kube` test spec structure\n\nAll `gdt` test specs have the same [base fields][base-spec-fields]:\n\n* `name`: (optional) string describing the test unit.\n* `description`: (optional) string with longer description of the test unit.\n* `timeout`: (optional) an object containing [timeout information][timeout] for the test\n  unit.\n* `timeout`: (optional) a string duration of time the test unit is expected to\n  complete within.\n* `retry`: (optional) an object containing retry configurationu for the test\n  unit. Some plugins will automatically attempt to retry the test action when\n  an assertion fails. This field allows you to control this retry behaviour for\n  each individual test.\n* `retry.interval`: (optional) a string duration of time that the test plugin\n  will retry the test action in the event assertions fail. The default interval\n  for retries is plugin-dependent.\n* `retry.attempts`: (optional) an integer indicating the number of times that a\n  plugin will retry the test action in the event assertions fail. The default\n  number of attempts for retries is plugin-dependent.\n* `retry.exponential`: (optional) a boolean indicating an exponential backoff\n  should be applied to the retry interval. The default is is plugin-dependent.\n* `wait` (optional) an object containing [wait information][wait] for the test\n  unit.\n* `wait.before`: a string duration of time that gdt should wait before\n  executing the test unit's action.\n* `wait.after`: a string duration of time that gdt should wait after executing\n  the test unit's action.\n\n[wait]: https://github.com/gdt-dev/core/blob/023f92ee3468852d1d477df91cf42789e472b3b5/api/wait.go#L11-L25\n\n`gdt-kube` test specs have some additional fields that allow you to take some\naction against a Kubernetes API and assert that the response from the API\nmatches some expectation:\n\n* `config`: (optional) file path to the `kubeconfig` to use for this specific\n  test. This allows you to override the `defaults.config` value from the test\n  scenario.\n* `context`: (optional) string containing the name of the kube context to use\n  for this specific test. This allows you to override the `defaults.context`\n  value from the test scenario.\n* `namespace`: (optional) string containing the name of the Kubernetes\n  namespace to use when performing some action for this specific test. This\n  allows you to override the `defaults.namespace` value from the test scenario.\n* `kube`: (optional) an object containing actions and assertions the test takes\n  against the Kubernetes API server.\n* `kube.get`: (optional) string or object containing a resource identifier\n  (e.g.  `pods`, `po/nginx` or [label selector](#using-label-selectors) for\n  resources that will be read from the Kubernetes API server.\n* `kube.create`: (optional) string containing either a file path to a YAML\n  manifest or a string of raw YAML containing the resource(s) to create.\n* `kube.apply`: (optional) string containing either a file path to a YAML\n  manifest or a string of raw YAML containing the resource(s) for which\n  `gdt-kube` will perform a Kubernetes Apply call.\n* `kube.delete`: (optional) string or object containing either a resource\n  identifier (e.g.  `pods`, `po/nginx` , a file path to a YAML manifest, or a\n  [label selector](#using-label-selectors) for resources that will be deleted.\n* `var`: (optional) an object describing variables that can have\n  values saved and referred to by subsequent test specs. Each key in the `var`\n  object is the name of the variable to define.\n* `var.$VARIABLE_NAME.from`: (required) a JSONPath expression describing where\n  the variable with name `$VARIABLE_NAME` should source its value from the\n  Kubernetes resource returned in a `kubectl get` response.\n* `assert`: (optional) object containing assertions to make about the\n  action performed by the test.\n* `assert.require`: (optional) a boolean indicating whether a failed assertion\n  will cause the test scenario's execution to stop. The default behaviour of\n  `gdt` is to continue execution of subsequent test specs in a test scenario when\n  an assertion fails.\n* `assert.error`: (optional) string to match a returned error from the\n  Kubernetes API server.\n* `assert.len`: (optional) int with the expected number of items returned.\n* `assert.notfound`: (optional) bool indicating the test author expects\n  the Kubernetes API to return a 404/Not Found for a resource.\n* `assert.unknown`: (optional) bool indicating the test author expects the\n  Kubernetes API server to respond that it does not know the type of resource\n  attempting to be fetched or created.\n* `assert.matches`: (optional) a YAML string, a filepath, or a\n  `map[string]interface{}` representing the content that you expect to find in\n  the returned result from the `kube.get` call. If `assert.matches` is a\n  string, the string can be either a file path to a YAML manifest or\n  inline an YAML string containing the resource fields to compare.\n  Only fields present in the Matches resource are compared. There is a\n  check for existence in the retrieved resource as well as a check that\n  the value of the fields match. Only scalar fields are matched entirely.\n  In other words, you do not need to specify every field of a struct field\n  in order to compare the value of a single field in the nested struct.\n* `assert.conditions`: (optional) a map, keyed by `ConditionType` string,\n  of any of the following:\n  - a string containing the `Status` value that the `Condition` with the\n    `ConditionType` should have.\n  - a list of strings containing the `Status` value that the `Condition` with\n    the `ConditionType` should have.\n  - an object containing two fields:\n    * `status` which itself is either a single string or a list of strings\n      containing the `Status` values that the `Condition` with the\n      `ConditionType` should have\n    * `reason` which is the exact string that should be present in the\n      `Condition` with the `ConditionType`\n* `assert.placement`: (optional) an object describing assertions to make about\n  the placement (scheduling outcome) of Pods returned in the `kube.get` result.\n* `assert.placement.spread`: (optional) an single string or array of strings\n  for topology keys that the Pods returned in the `kube.get` result should be\n  spread evenly across, e.g. `topology.kubernetes.io/zone` or\n  `kubernetes.io/hostname`.\n* `assert.placement.pack`: (optional) an single string or array of strings for\n  topology keys that the Pods returned in the `kube.get` result should be\n  bin-packed within, e.g. `topology.kubernetes.io/zone` or\n  `kubernetes.io/hostname`.\n* `assert.json`: (optional) object describing the assertions to make about\n  resource(s) returned from the `kube.get` call to the Kubernetes API server.\n* `assert.json.len`: (optional) integer representing the number of bytes in the\n  resulting JSON object after successfully parsing the resource.\n* `assert.json.paths`: (optional) map of strings where the keys of the map\n  are JSONPath expressions and the values of the map are the expected value to\n  be found when evaluating the JSONPath expression\n* `assert.json.path-formats`: (optional) map of strings where the keys of the map are\n  JSONPath expressions and the values of the map are the expected format of the\n  value to be found when evaluating the JSONPath expression. See the\n  [list of valid format strings](#valid-format-strings)\n* `assert.json.schema`: (optional) string containing a filepath to a\n  JSONSchema document.  If present, the resource's structure will be validated\n  against this JSONSChema document.\n\n## Examples\n\nHere are some examples of `gdt-kube` tests.\n\nTesting that a Pod with the name `nginx` exists:\n\n```yaml\nname: test-nginx-pod-exists\ntests:\n - kube:\n     get: pods/nginx\n # These are equivalent. \"kube.get\" is a shortcut for the longer object.field\n # form above.\n - kube.get: pods/nginx\n```\n\nTesting that a Pod with the name `nginx` *does not* exist:\n\n```yaml\nname: test-nginx-pod-not-exist\ntests:\n - kube:\n     get: pods/nginx\n   assert:\n     notfound: true\n```\n\nTesting that there are two Pods having the label `app:nginx`:\n\n```yaml\nname: list-pods-with-labels\ntests:\n  # You can use the shortcut kube.get\n  - name: verify-pods-with-app-nginx-label\n    kube.get:\n      type: pods\n      labels:\n        app: nginx\n    assert:\n      len: 2\n  # Or the long-form kube:get\n  - name: verify-pods-with-app-nginx-label\n    kube:\n      get:\n        type: pods\n        labels:\n          app: nginx\n    assert:\n      len: 2\n  # Like \"kube.get\", you can pass a label selector for \"kube.delete\"\n  - kube.delete:\n      type: pods\n      labels:\n        app: nginx\n  # And you can use the long-form kube:delete as well\n  - kube:\n      delete:\n        type: pods\n        labels:\n          app: nginx\n```\n\nTesting that a Pod with the name `nginx` exists by the specified timeout\n(essentially, `gdt-kube` will retry the get call and assertion until the end of\nthe timeout):\n\n```yaml\nname: test-nginx-pod-exists-within-1-minute\ntests:\n - kube.get: pods/nginx\n   timeout: 1m\n```\n\nTesting creation and subsequent fetch then delete of a Pod, specifying the Pod\ndefinition contained in a YAML file:\n\n```yaml\nname: create-get-delete-pod\ndescription: create, get and delete a Pod\nfixtures:\n  - kind\ntests:\n  - name: create-pod\n    kube:\n      create: manifests/nginx-pod.yaml\n  - name: pod-exists\n    kube:\n      get: pods/nginx\n  - name: delete-pod\n    kube:\n      delete: pods/nginx\n```\n\nTesting creation and subsequent fetch then delete of a Pod, specifying the Pod\ndefinition using an inline YAML blob:\n\n```yaml\nname: create-get-delete-pod\ndescription: create, get and delete a Pod\nfixtures:\n  - kind\ntests:\n  # \"kube.create\" is a shortcut for the longer object-\u003efield format\n  - kube.create: |\n        apiVersion: v1\n        kind: Pod\n        metadata:\n          name: nginx\n        spec:\n          containers:\n          - name: nginx\n            image: nginx\n            imagePullPolicy: IfNotPresent\n  # \"kube.get\" is a shortcut for the longer object-\u003efield format\n  - kube.get: pods/nginx\n  # \"kube.delete\" is a shortcut for the longer object-\u003efield format\n  - kube.delete: pods/nginx\n```\n\n### Using label selectors\n\nWhen selecting objects from the Kubernetes API, you can use a label selector in\nthe `kube.get` and `kube.delete` test spec actions. This label selector\nfunctionality is incredibly flexible. You can use a `kubectl`-style label\nselector string, like so:\n\n```yaml\n - name: select pods that have the \"app=argo\" label but do NOT have the \"app=argo-rollouts\" or \"app=argorollouts\" label\n   kube:\n     get:\n       type: pod\n       labels: app in (argo),app notin (argo-rollouts,argorollouts)\n```\n\nThe `kube.get.labels` field can also be a map of string to string, which is\nmore aligned with how `gdt`'s YAML syntax is structured. This example selects\npods that have **both** the `app=myapp` **and** the `region=myregion` label:\n\n```yaml\n - name: select pods that have BOTH the app=myapp AND region=myregion label\n   kube:\n     get:\n       type: pod\n       labels:\n         app: myapp\n         region: myregion\n```\n\nThe `kube.get.labels-in` field is a map of string to slice of strings and gets\ntranslated into a \"label IN (val1, val2)\" expression. This example selects pods\nthat have **either** the `app=myapp` label **or** the `app=test` label:\n\n```yaml\n - name: select pods that have EITHER the app=myapp OR app=test label\n   kube:\n     get:\n       type: pod\n       labels-in:\n         app:\n          - myapp\n          - test\n```\n\nThe `kube.get.labels-not-in` field is also a map of string to slice of strings\nand gets translated into a `label NOTIN (val1, val2)` expression. This example\nselects pods that **do not** have either the `app=myapp` or the `app=test`\nlabel.\n\n```yaml\n - name: select pods that have DON'T HAVE the app=myapp OR app=test label\n   kube:\n     get:\n       type: pod\n       labels-not-in:\n         app:\n          - myapp\n          - test\n```\n\nYou can combine the `kube.get.labels`, `kube.get.labels-in` and\n`kube.get.labels-not-in` fields to create complex querying expressions. This\nexample selects pods that have the `app=myapp` label **and** have **either**\nthe `category=test` or `category=staging` label **and** do **not** have a\n`personal=true` label:\n\n```yaml\n - 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\n   kube:\n     get:\n       type: pod\n       labels:\n         app: myapp\n       labels-in:\n         category:\n          - test\n          - staging\n       labels-not-in:\n         personal:\n          - true\n```\n\n### Passing variables to subsequent test specs\n\nA `gdt` test scenario is comprised of a list of test specs. These test specs\nare executed in sequential order. If you want to have one test spec be able to\nuse some output or value calculated or asserted in a previous step, you can use\nthe `gdt` variable system.\n\nHere's a test scenario that shows how to define variables in a test spec and\nhow to use those variables in later test specs.\n\nfile: `testdata/var-save-restore.yaml`:\n\n```yaml\nname: var-save-restore\ndescription: scenario showing different variations of variable definition and references\ndefaults:\n  kube:\n    namespace: var-save-restore\ntests:\n  - name: create-pod\n    kube:\n      create: testdata/manifests/nginx-pod.yaml\n\n  - name: define a variable and populate it with the pod's IP address\n    kube:\n      get: pods/nginx\n    assert:\n      conditions:\n        ready:\n          status: true\n    var:\n      POD_IP:\n        from: $.status.podIP\n      POD_NAME:\n        from: $.metadata.name\n\n  - name: get the pod's information and assert same values as variables\n    kube.get: pods/$$POD_NAME\n    assert:\n      matches:\n        status:\n          podIP: $$POD_IP\n\n  - name: delete-pod\n    kube:\n      delete: pods/$$POD_NAME\n```\n\nIn the first test spec, we create new Pod by specifying the filepath to a\nKubernetes manifest (`testdata/manifests/nginx-pod.yaml`):\n\n```yaml\n  - name: create-pod\n    kube:\n      create: testdata/manifests/nginx-pod.yaml\n```\n\nIn the second test spec, we get the Pod that we created in the first step,\nasserting that the Pod is in a Ready state:\n\n```yaml\n  - name: define a variable and populate it with the pod's IP address\n    kube:\n      get: pods/nginx\n    assert:\n      conditions:\n        ready:\n          status: true\n```\n\nThe `assert.conditions.ready.status=true` means that the above test spec will\nnot succeed until the Pod is in a Ready state.\n\n\u003e **NOTE**: Pods that are in a Ready state have their `status.podIP` field set\n\u003e to a non-empty value.\n\nIn the same second test spec, we add a `var:` section to define two variables\nthat we can refer to in subsequent test specs:\n\n```yaml\n    var:\n      POD_IP:\n        from: $.status.podIP\n      POD_NAME:\n        from: $.metadata.name\n```\n\nThe above creates two variables. The first variable is named `POD_IP` and will\nget its value from the field in the returned Pod resource representation at the\nJSONPath `$.status.podIP`. The second variable is named `POD_NAME` and gets its\nvalue from the field in the Pod resource representation at the JSONPath\n`$.metadata.name`.\n\n\u003e **NOTE**: People familiar with using `kubectl get ... -o jsonpath=EXPRESSION`\n\u003e should find this syntax easy to understand. Just remember that in gdt (and\n\u003e proper JSONPath expressions), you must begin the JSONPath expression with the\n\u003e `$.` characters instead of the `kubectl` behaviour of beginning the JSONPath\n\u003e expression with the `{.` characters.\n\u003e\n\u003e If you do something like this to get a Pod's IP using `kubectl`:\n\u003e\n\u003e `kubectl get pod/my-pod -o jsonpath='{.status.podIP}'`\n\u003e\n\u003e You would do the following in a `gdt-kube` test spec:\n\u003e\n\u003e `from: $.status.podIP`\n\nHaving defined the `POD_IP` and `POD_NAME` variables, you can then refer to the\nvalues contained in those variables in subsequent `gdt` test specs by using the\ndouble-dollar-sign notation.\n\n\u003e **NOTE**: We use the double-dollar-sign notation because by default, `gdt`\n\u003e replaces all single-dollar-sign notations with environment variables *BEFORE*\n\u003e executing the test specs in a test scenario. Using the double-dollar-sign\n\u003e notation means that environment variable substitution does not impact the\n\u003e referencing of `gdt` variables referenced in a test spec.\n\nIn the third test spec, you see an example of referring to the `POD_NAME`\nvariable in the `kube.get` field:\n\n```yaml\n  - name: get the pod's information and assert same values as variables\n    kube.get: pods/$$POD_NAME\n```\n\nas well as an example of referring to the `POD_IP` variable in the `assert`\nfield of that same test spec:\n\n```yaml\n    assert:\n      matches:\n        status:\n          podIP: $$POD_IP\n```\n\nFinally, the fourth test spec demonstrates that you can continue to refer to a\nvariable defined in any previous test spec. In the fourth test spec, we refer\nto the `POD_NAME` variable from the `kube.delete` field:\n\n```yaml\n  - name: delete-pod\n    kube:\n      delete: pods/$$POD_NAME\n```\n\n### Executing arbitrary commands or shell scripts\n\nYou can mix other `gdt` test types in a single `gdt` test scenario. For\nexample, here we are testing the creation of a Pod, waiting a little while with\nthe `wait.after` directive, then using the `gdt` `exec` test type to test SSH\nconnectivity to the Pod.\n\n```yaml\nname: create-check-ssh\ndescription: create a Deployment then check SSH connectivity\nfixtures:\n  - kind\ntests:\n  - kube.create: manifests/deployment.yaml\n    wait:\n      after: 30s\n  - exec: ssh -T someuser@ip\n```\n\nNote that you can make use of the `gdt` variable system to pass values between\n`gdt` test specs, as this example demonstrates:\n\nfile: `testdata/curl-pod-ip.yaml`\n\n```yaml\nname: curl-pod-ip\ndescription: scenario showing how to create two NGinx Pods and test connectivity to internal Pod IP addresses\nfixtures:\n  - kind\ndefaults:\n  kube:\n    namespace: curl-pod-ip\ntests:\n  - name: create-server\n    kube:\n      create: testdata/manifests/nginx-server.yaml\n\n  - name: get-server-pod-ip\n    kube:\n      get: pods/server\n    assert:\n      conditions:\n        ready:\n          status: true\n    var:\n      SERVER_IP:\n        from: $.status.podIP\n\n  - name: create-connect-tester\n    kube:\n      create: testdata/manifests/nginx-connect-test.yaml\n\n  - name: wait-connect-test-ready\n    kube:\n      get: pods/connect-test\n    assert:\n      conditions:\n        ready:\n          status: true\n\n  - name: curl-server-from-connect-tester\n    exec: kubectl exec -n curl-pod-ip pods/connect-test -- curl -s -I -v $$SERVER_IP\n    timeout: 2s\n    assert:\n      out:\n        contains: \"200 OK\"\n      # curl -v causes output to be sent to stderr that looks like this:\n      # * Connected to 10.244.0.17 (10.244.0.17) port 80 (#0)\n      # \u003e GET / HTTP/1.1\n      # \u003e Host: 10.244.0.17\n      # \u003e User-Agent: curl/7.88.1\n      # \u003e Accept: */*\n      # \u003e\n      # \u003c HTTP/1.1 200 OK\n      err:\n        contains: \"Host: $$SERVER_IP\"\n\n  - name: delete-connect-tester\n    kube:\n      delete: pods/connect-test\n\n  - name: delete-server\n    kube:\n      delete: pods/server\n```\n\nIn the above example, we use a few `gdt-kube` test specs (the ones with `kube:`\nfield in the test spec definition) along with an `exec` test spec that executes\n`curl` from a test Pod that a previous test spec creates and the command that\nthe test spec executes references the `SERVER_IP` variable defined in the\nsecond step:\n\n```yaml\n  - name: curl-server-from-connect-tester\n    exec: kubectl exec -n curl-pod-ip pods/connect-test -- curl -s -I -v $$SERVER_IP\n```\n\nYou can see that the `assert:` field in the `exec` test spec references the\n`SERVER_IP` variable:\n\n```yaml\n      err:\n        contains: \"Host: $$SERVER_IP\"\n```\n\n### Asserting resource fields using `assert.matches`\n\nThe `assert.matches` field of a `gdt-kube` test Spec allows a test author\nto specify expected fields and those field contents in a resource that was\nreturned by the Kubernetes API server from the result of a `kube.get` call.\n\nSuppose you have a Deployment resource and you want to write a test that checks\nthat a Deployment resource's `Status.ReadyReplicas` field is `2`.\n\nYou do not need to specify all other `Deployment.Status` fields like\n`Status.Replicas` in order to match the `Status.ReadyReplicas` field value. You\nonly need to include the `Status.ReadyReplicas` field in the `Matches` value as\nthese examples demonstrate:\n\n```yaml\ntests:\n - name: check deployment's ready replicas is 2\n   kube:\n     get: deployments/my-deployment\n   assert:\n     matches: |\n       kind: Deployment\n       metadata:\n         name: my-deployment\n       status:\n         readyReplicas: 2\n```\n\nyou don't even need to include the kind and metadata in `assert.matches`.\nIf missing, no kind and name matching will be performed.\n\n```yaml\ntests:\n - name: check deployment's ready replicas is 2\n   kube:\n     get: deployments/my-deployment\n   assert:\n     matches: |\n       status:\n         readyReplicas: 2\n```\n\nIn fact, you don't need to use an inline multiline YAML string. You can\nuse a `map[string]interface{}` as well:\n\n```yaml\ntests:\n - name: check deployment's ready replicas is 2\n   kube:\n     get: deployments/my-deployment\n   assert:\n     matches:\n       status:\n         readyReplicas: 2\n```\n\n### Asserting resource `Conditions` using `assert.conditions`\n\n`assertion.conditions` contains the assertions to make about a resource's\n`Status.Conditions` collection. It is a map, keyed by the ConditionType\n(matched case-insensitively), of assertions to make about that Condition. The\nassertions can be:\n\n* a string which is the ConditionStatus that should be found for that\n  Condition\n* a list of strings containing ConditionStatuses, any of which should be\n  found for that Condition\n* an object of type `ConditionExpect` that contains more fine-grained\n  assertions about that Condition's Status and Reason\n\nA simple example that asserts that a Pod's `Ready` Condition has a\nstatus of `True`. Note that both the condition type (\"Ready\") and the\nstatus (\"True\") are matched case-insensitively, which means you can just\nuse lowercase strings:\n\n```yaml\ntests:\n - kube:\n     get: pods/nginx\n   assert:\n     conditions:\n       ready: true\n```\n\nIf we wanted to assert that the `ContainersReady` Condition had a status\nof either `False` or `Unknown`, we could write the test like this:\n\n```yaml\ntests:\n - kube:\n     get: pods/nginx\n   assert:\n     conditions:\n       containersReady:\n        - false\n        - unknown\n```\n\nFinally, if we wanted to assert that a Deployment's `Progressing`\nCondition had a Reason field with a value \"NewReplicaSetAvailable\"\n(matched case-sensitively), we could do the following:\n\n```yaml\ntests:\n - kube:\n     get: deployments/nginx\n   assert:\n     conditions:\n       progressing:\n         status: true\n         reason: NewReplicaSetAvailable\n```\n\n### Asserting scheduling outcomes using `assert.placement`\n\nThe `assert.placement` field of a `gdt-kube` test Spec allows a test author to\nspecify the expected scheduling outcome for a set of Pods returned by the\nKubernetes API server from the result of a `kube.get` call.\n\n#### Asserting even spread of Pods across a topology\n\nSuppose you have a Deployment resource with a `TopologySpreadConstraints` that\nspecifies the Pods in the Deployment must land on different hosts:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  labels:\n    app: nginx\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n       - name: nginx\n         image: nginx:latest\n         ports:\n          - containerPort: 80\n      topologySpreadConstraints:\n       - maxSkew: 1\n         topologyKey: kubernetes.io/hostname\n         whenUnsatisfiable: DoNotSchedule\n         labelSelector:\n           matchLabels:\n             app: nginx\n```\n\nYou can create a `gdt-kube` test case that verifies that your `nginx`\nDeployment's Pods are evenly spread across all available hosts:\n\n```yaml\ntests:\n - kube:\n     get: deployments/nginx\n   assert:\n     placement:\n       spread: kubernetes.io/hostname\n```\n\nIf there are more hosts than the `spec.replicas` in the Deployment, `gdt-kube`\nwill ensure that each Pod landed on a unique host. If there are fewer hosts\nthan the `spec.replicas` in the Deployment, `gdt-kube` will ensure that there\nis an even spread of Pods to hosts, with any host having no more than one more\nPod than any other.\n\n#### Asserting bin-packing of Pods\n\nSuppose you have configured your Kubernetes scheduler to bin-pack Pods onto\nhosts by scheduling Pods to hosts with the most allocated CPU resources:\n\n```yaml\napiVersion: kubescheduler.config.k8s.io/v1\nkind: KubeSchedulerConfiguration\nprofiles:\n- pluginConfig:\n  - args:\n      scoringStrategy:\n        resources:\n        - name: cpu\n          weight: 100\n        type: MostAllocated\n    name: NodeResourcesFit\n```\n\nYou can create a `gdt-kube` test case that verifies that your `nginx`\nDeployment's Pods are packed onto the fewest unique hosts:\n\n```yaml\ntests:\n - kube:\n     get: deployments/nginx\n   assert:\n     placement:\n       pack: kubernetes.io/hostname\n```\n\n`gdt-kube` will examine the total number of hosts that meet the nginx\nDeployment's scheduling and resource constraints and then assert that the\nnumber of hosts the Deployment's Pods landed on is the minimum number that\nwould fit the total requested resources.\n\n### Asserting resource fields using `assert.json`\n\nThe `assert.json` field of a `gdt-kube` test Spec allows a test author to\nspecify expected fields, the value of those fields as well as the format of\nfield values in a resource that was returned by the Kubernetes API server from\nthe result of a `kube.get` call.\n\nSuppose you have a Deployment resource and you want to write a test that checks\nthat a Deployment resource's `Status.ReadyReplicas` field is `2`.\n\nYou can specify this expectation using the `assert.json.paths` field,\nwhich is a `map[string]interface{}` that takes map keys that are JSONPath\nexpressions and map values of what the field at that JSONPath expression should\ncontain:\n\n```yaml\ntests:\n - name: check deployment's ready replicas is 2\n   kube:\n     get: deployments/my-deployment\n   assert:\n     json:\n       paths:\n         $.status.readyReplicas: 2 \n```\n\nJSONPath expressions can be fairly complex, allowing the test author to, for\nexample, assert the value of a nested map field with a particular key, as this\nexample shows:\n\n```yaml\ntests:\n - name: check deployment's pod template \"app\" label is \"nginx\"\n   kube:\n     get: deployments/my-deployment\n   assert:\n     json:\n       paths:\n         $.spec.template.labels[\"app\"]: nginx\n```\n\nYou can check that the value of a particular field at a JSONPath is formatted\nin a particular fashion using `assert.json.path-formats`. This is a map,\nkeyed by JSONPath expression, of the data format the value of the field at that\nJSONPath expression should have. Valid data formats are:\n\n* `date`\n* `date-time`\n* `email`\n* `hostname`\n* `idn-email`\n* `ipv4`\n* `ipv6`\n* `iri`\n* `iri-reference`\n* `json-pointer`\n* `regex`\n* `relative-json-pointer`\n* `time`\n* `uri`\n* `uri-reference`\n* `uri-template`\n* `uuid`\n* `uuid4`\n\n[Read more about JSONSchema formats](https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats).\n\nFor example, suppose we wanted to verify that a Deployment's `metadata.uid`\nfield was a UUID-4 and that its `metadata.creationTimestamp` field was a\ndate-time timestamp:\n\n```yaml\ntests:\n  - kube:\n      get: deployments/nginx\n    assert:\n      json:\n        path-formats:\n          $.metadata.uid: uuid4\n          $.metadata.creationTimestamp: date-time\n```\n\n### Updating a resource and asserting corresponding field changes\n\nHere is an example of creating a Deployment with an initial `spec.replicas`\ncount of 2, then applying a change to `spec.replicas` of 1, then asserting that\nthe `status.readyReplicas` gets updated to 1.\n\nfile `testdata/manifests/nginx-deployment.yaml`:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx\n        ports:\n        - containerPort: 80\n```\n\nfile `testdata/apply-deployment.yaml`:\n\n```yaml\nname: apply-deployment\ndescription: create, get, apply a change, get, delete a Deployment\nfixtures:\n  - kind\ntests:\n  - name: create-deployment\n    kube:\n      create: testdata/manifests/nginx-deployment.yaml\n  - name: deployment-has-2-replicas\n    timeout:\n      after: 20s\n    kube:\n      get: deployments/nginx\n    assert:\n      matches:\n        status:\n          readyReplicas: 2\n  - name: apply-deployment-change\n    kube:\n      apply: |\n        apiVersion: apps/v1\n        kind: Deployment\n        metadata:\n          name: nginx\n        spec:\n          replicas: 1\n  - name: deployment-has-1-replica\n    timeout:\n      after: 20s\n    kube:\n      get: deployments/nginx\n    assert:\n      matches:\n        status:\n          readyReplicas: 1\n  - name: delete-deployment\n    kube:\n      delete: deployments/nginx\n```\n\n## Determining Kubernetes config, context and namespace values\n\nWhen evaluating how to construct a Kubernetes client `gdt-kube` uses the following\nprecedence to determine the `kubeconfig` and kube context:\n\n1) The individual test spec's `config` or `context` value\n2) Any `gdt` Fixture that exposes a `gdt.kube.config` or `gdt.kube.context`\n   state key (e.g. [`KindFixture`][kind-fixture]).\n3) The test file's `defaults.kube` `config` or `context` value.\n\nFor the `kubeconfig` file path, if none of the above yielded a value, the\nfollowing precedence is used to determine the `kubeconfig`:\n\n4) A non-empty `KUBECONFIG` environment variable pointing at a file.\n5) In-cluster config if running in cluster.\n6) `$HOME/.kube/config` if it exists.\n\n[kube-fixture]: https://github.com/gdt-dev/kube/blob/main/fixtures/kind/kind.go\n\n## `gdt-kube` Fixtures\n\n`gdt` Fixtures are objects that help set up and tear down a testing\nenvironment. The `gdt-kube` library has some utility fixtures to make testing\nwith Kubernetes easier.\n\n### `KindFixture`\n\nThe `KindFixture` eases integration of `gdt-kube` tests with the KinD local\nKubernetes development system.\n\nTo use it, import the `gdt-kube/fixtures/kind` package:\n\n```go\nimport (\n    \"github.com/gdt-dev/gdt\"\n    gdtkube \"github.com/gdt-dev/kube\"\n    gdtkind \"github.com/gdt-dev/kube/fixtures/kind\"\n)\n```\n\nand then register the fixture with your `gdt` `Context`, like so:\n\n```go\nfunc TestExample(t *testing.T) {\n    s, err := gdt.From(\"path/to/test.yaml\")\n    if err != nil {\n        t.Fatalf(\"failed to load tests: %s\", err)\n    }\n\n    ctx := context.Background()\n    ctx = gdt.RegisterFixture(ctx, \"kind\", gdtkind.New())\n    err = s.Run(ctx, t)\n    if err != nil {\n        t.Fatalf(\"failed to run tests: %s\", err)\n    }\n}\n```\n\nIn your test file, you would list the \"kind\" fixture in the `fixtures` list:\n\n```yaml\nname: example-using-kind\nfixtures:\n - kind\ntests:\n - kube.get: pods/nginx\n```\n\n#### Retaining and deleting KinD clusters\n\nThe default behaviour of the `KindFixture` is to delete the KinD cluster when\nthe Fixture's `Stop()` method is called, but **only if the KinD cluster did not\npreviously exist before the Fixture's `Start()` method was called**.\n\nIf you want to *always* ensure that a KinD cluster is deleted when the\n`KindFixture` is stopped, use the `fixtures.kind.WithDeleteOnStop()` function:\n\n```go\nimport (\n    \"github.com/gdt-dev/gdt\"\n    gdtkube \"github.com/gdt-dev/kube\"\n    gdtkind \"github.com/gdt-dev/kube/fixtures/kind\"\n)\n\nfunc TestExample(t *testing.T) {\n    s, err := gdt.From(\"path/to/test.yaml\")\n    if err != nil {\n        t.Fatalf(\"failed to load tests: %s\", err)\n    }\n\n    ctx := context.Background()\n    ctx = gdt.RegisterFixture(\n        ctx, \"kind\", gdtkind.New(),\n        gdtkind.WithDeleteOnStop(),\n    )\n    err = s.Run(ctx, t)\n    if err != nil {\n        t.Fatalf(\"failed to run tests: %s\", err)\n    }\n}\n```\n\nLikewise, the default behaviour of the `KindFixture` is to retain the KinD\ncluster when the Fixture's `Stop()` method is called but **only if the KinD\ncluster previously existed before the Fixture's `Start()` method was called**.\n\nIf you want to *always* ensure a KinD cluster is retained, even if the\nKindFixture created the KinD cluster, use the\n`fixtures.kind.WithRetainOnStop()` function:\n\n```go\nimport (\n    \"github.com/gdt-dev/gdt\"\n    gdtkube \"github.com/gdt-dev/kube\"\n    gdtkind \"github.com/gdt-dev/kube/fixtures/kind\"\n)\n\nfunc TestExample(t *testing.T) {\n    s, err := gdt.From(\"path/to/test.yaml\")\n    if err != nil {\n        t.Fatalf(\"failed to load tests: %s\", err)\n    }\n\n    ctx := context.Background()\n    ctx = gdt.RegisterFixture(\n        ctx, \"kind\", gdtkind.New(),\n        gdtkind.WithRetainOnStop(),\n    )\n    err = s.Run(ctx, t)\n    if err != nil {\n        t.Fatalf(\"failed to run tests: %s\", err)\n    }\n}\n```\n\n#### Passing a KinD configuration\n\nYou may want to pass a custom KinD configuration resource by using the\n`fixtures.kind.WithConfigPath()` modifier:\n\n```go\nimport (\n    \"github.com/gdt-dev/gdt\"\n    gdtkube \"github.com/gdt-dev/kube\"\n    gdtkind \"github.com/gdt-dev/kube/fixtures/kind\"\n)\n\nfunc TestExample(t *testing.T) {\n    s, err := gdt.From(\"path/to/test.yaml\")\n    if err != nil {\n        t.Fatalf(\"failed to load tests: %s\", err)\n    }\n\n    configPath := filepath.Join(\"testdata\", \"my-kind-config.yaml\")\n\n    ctx := context.Background()\n    ctx = gdt.RegisterFixture(\n        ctx, \"kind\", gdtkind.New(),\n        gdtkind.WithConfigPath(configPath),\n    )\n    err = s.Run(ctx, t)\n    if err != nil {\n        t.Fatalf(\"failed to run tests: %s\", err)\n    }\n}\n```\n\n## Contributing and acknowledgements\n\n`gdt` was inspired by [Gabbi](https://github.com/cdent/gabbi), the excellent\nPython declarative testing framework. `gdt` tries to bring the same clear,\nconcise test definitions to the world of Go functional testing.\n\nThe Go gopher logo, from which gdt's logo was derived, was created by Renee\nFrench.\n\nContributions to `gdt-kube` are welcomed! Feel free to open a Github issue or\nsubmit a pull request.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgdt-dev%2Fkube","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgdt-dev%2Fkube","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgdt-dev%2Fkube/lists"}