├── testdata ├── config │ ├── empty.yaml │ ├── configmap.yaml │ ├── default.yaml │ ├── multiple.yaml │ └── custom-config.yaml ├── resource │ ├── empty.yaml │ ├── invalid.yaml │ ├── folder-invalid │ │ ├── invalid.yaml │ │ └── custom-resource.yaml │ ├── valid.yaml │ ├── folder-valid │ │ ├── valid.yaml │ │ └── custom-resource.yaml │ ├── list.yaml │ └── custom-resource.yaml ├── test │ ├── empty.yaml │ ├── configmap.yaml │ ├── no-spec.yaml │ ├── no-steps.yaml │ ├── bad-step.yaml │ ├── multiple.yaml │ ├── raw-resource.yaml │ ├── ok.yaml │ └── custom-test.yaml ├── values │ ├── empty.yaml │ ├── invalid.yaml │ ├── values-1.yaml │ └── values-2.yaml ├── e2e │ ├── values.yaml │ ├── examples │ │ ├── non-resource-assertion │ │ │ ├── assert.yaml │ │ │ ├── error.yaml │ │ │ ├── chainsaw-test.yaml │ │ │ └── README.md │ │ ├── deployment │ │ │ ├── assertions.yaml │ │ │ ├── resources.yaml │ │ │ ├── chainsaw-test.yaml │ │ │ └── README.md │ │ ├── basic │ │ │ ├── configmap.yaml │ │ │ ├── configmap-assert.yaml │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── apply-outputs │ │ │ └── resources.yaml │ │ ├── list │ │ │ ├── list.yaml │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── assertion-tree │ │ │ ├── chainsaw-test.yaml │ │ │ ├── assert.yaml │ │ │ └── README.md │ │ ├── values │ │ │ ├── chainsaw-test.yaml │ │ │ └── README.md │ │ ├── array-assertion │ │ │ ├── chainsaw-test.yaml │ │ │ ├── resources.yaml │ │ │ ├── README.md │ │ │ └── assertions.yaml │ │ ├── timeout │ │ │ ├── chainsaw-test.yml │ │ │ └── README.md │ │ ├── sleep │ │ │ ├── chainsaw-test.yaml │ │ │ └── README.md │ │ ├── namespace-template │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── delete │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── inline │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── command-output │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── patch │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── k8s-server-version │ │ │ ├── chainsaw-test.yaml │ │ │ └── README.md │ │ ├── update │ │ │ └── README.md │ │ ├── orphan │ │ │ └── README.md │ │ ├── template │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── update-crd │ │ │ └── README.md │ │ ├── catch │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── finally │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ ├── script-env │ │ │ ├── README.md │ │ │ └── chainsaw-test.yaml │ │ └── bindings │ │ │ └── README.md │ ├── config.yaml │ └── config-v1alpha2.yaml ├── commands │ ├── docs │ │ ├── invalid-output.txt │ │ └── help.txt │ ├── migrate │ │ ├── kuttl │ │ │ ├── config │ │ │ │ ├── out-save.txt │ │ │ │ ├── help.txt │ │ │ │ └── out.txt │ │ │ ├── tests │ │ │ │ ├── out-save.txt │ │ │ │ └── help.txt │ │ │ └── help.txt │ │ └── help.txt │ ├── lint │ │ ├── test │ │ │ ├── pass.txt │ │ │ ├── txt.txt │ │ │ ├── test.yaml │ │ │ └── wrong-test.yaml │ │ └── configuration │ │ │ ├── pass.txt │ │ │ ├── configuration.yaml │ │ │ ├── wrong-configuration.yaml │ │ │ ├── configuration.json │ │ │ └── wrong-configuration.json │ ├── version │ │ ├── out.txt │ │ └── help.txt │ ├── test │ │ ├── wrong_kind_config.txt │ │ ├── wrong_format_config.txt │ │ ├── config │ │ │ ├── wrong_kind_config.yaml │ │ │ ├── empty_config.yaml │ │ │ ├── wrong_format_config.yaml │ │ │ └── config_all_fields.yaml │ │ ├── wrong_kind_config_err.txt │ │ ├── default.txt │ │ ├── with_timeout.txt │ │ ├── with_test_dirs.txt │ │ ├── with_regex.txt │ │ ├── with_suppress.txt │ │ ├── with_repeat_count.txt │ │ ├── all_flags.txt │ │ └── config_all_fields.txt │ ├── assert │ │ └── assert.yaml │ ├── root │ │ └── help.txt │ ├── export │ │ ├── schemas │ │ │ └── help.txt │ │ └── help.txt │ ├── build │ │ ├── help.txt │ │ └── docs │ │ │ └── help.txt │ ├── create │ │ ├── help.txt │ │ └── test │ │ │ └── help.txt │ └── help.txt ├── discovery │ ├── empty-test │ │ └── fake.json │ ├── manifests │ │ ├── 01-assert.yaml │ │ ├── 01-configmap.yaml │ │ └── 01-errors.yaml │ ├── not-a-test │ │ └── configmap.yaml │ ├── test-yml │ │ └── chainsaw-test.yml │ ├── test │ │ └── chainsaw-test.yaml │ └── multiple-tests │ │ └── chainsaw-test.yaml ├── kuttl │ ├── invalid-config.yaml │ ├── configmap.yaml │ ├── kuttl-test.yaml │ ├── 01-assert.yaml │ ├── 02-assert.yaml │ ├── multiple-config.yaml │ ├── .chainsaw.yaml │ ├── 01-step.yaml │ └── 02-step.yaml ├── validation │ └── example-file.yaml └── runner │ └── processors │ ├── pod.yaml │ ├── deployment.yaml │ └── cron-job.yaml ├── pkg ├── apis │ ├── doc.go │ ├── v1alpha1 │ │ ├── match.go │ │ ├── check.go │ │ ├── wait_deletion.go │ │ ├── any.go │ │ ├── format.go │ │ ├── doc.go │ │ ├── sleep.go │ │ ├── test_spec_step.go │ │ ├── cluster.go │ │ ├── wait_jsonpath.go │ │ ├── file_ref.go │ │ ├── output.go │ │ ├── object_type.go │ │ ├── file_ref_or_check.go │ │ ├── expectation.go │ │ ├── wait_condition.go │ │ ├── object_reference.go │ │ ├── wait_for.go │ │ ├── test.go │ │ ├── file_ref_or_resource.go │ │ ├── resource_reference.go │ │ ├── configuration.go │ │ ├── binding.go │ │ ├── object_selector.go │ │ ├── object_labels_selector.go │ │ ├── events.go │ │ └── get.go │ └── v1alpha2 │ │ └── doc.go ├── runner │ ├── logging │ │ ├── t_logger.go │ │ ├── testing │ │ │ ├── fake_t_logger.go │ │ │ ├── logger.go │ │ │ └── fake_logger.go │ │ ├── log.go │ │ ├── context.go │ │ ├── section.go │ │ └── logger.go │ ├── operations │ │ ├── internal │ │ │ ├── interval.go │ │ │ ├── namespace.go │ │ │ ├── command_output.go │ │ │ └── env.go │ │ ├── operation.go │ │ ├── testing │ │ │ └── mock.go │ │ └── sleep │ │ │ └── operation.go │ ├── cleanup │ │ ├── skip.go │ │ └── cleaner.go │ ├── processors │ │ └── info.go │ ├── functions │ │ ├── functions_test.go │ │ ├── env.go │ │ ├── caller.go │ │ └── utils.go │ ├── template │ │ └── template.go │ ├── timeout │ │ ├── timeout.go │ │ └── timeout_test.go │ ├── failer │ │ └── context.go │ ├── clusters │ │ ├── register.go │ │ ├── resolve.go │ │ └── client.go │ ├── internal │ │ ├── flags.go │ │ └── corpus_entry.go │ ├── env │ │ ├── expand.go │ │ └── expand_test.go │ ├── namespacer │ │ ├── testing │ │ │ └── fake_namespacer.go │ │ └── namespacer.go │ ├── summary │ │ ├── summary.go │ │ └── summary_test.go │ ├── mutate │ │ ├── convert_test.go │ │ ├── convert.go │ │ └── merge.go │ ├── flags │ │ └── flags.go │ ├── bindings │ │ └── string.go │ └── check │ │ └── check.go ├── data │ ├── config │ │ └── default.yaml │ └── data.go ├── client │ ├── auth.go │ ├── pet.go │ ├── unstructured.go │ ├── namespace.go │ ├── namespace_test.go │ ├── patch.go │ ├── unstructured_test.go │ ├── client_test.go │ └── key.go ├── commands │ ├── build │ │ ├── docs │ │ │ └── catalog.tmpl │ │ └── command.go │ ├── root │ │ └── command.go │ ├── export │ │ └── command.go │ ├── create │ │ └── command.go │ ├── migrate │ │ ├── command.go │ │ └── kuttl │ │ │ └── command.go │ ├── version │ │ └── command.go │ ├── lint │ │ ├── schema.go │ │ └── processor.go │ ├── docs │ │ └── utils.go │ └── root.go ├── discovery │ ├── test.go │ └── discovery.go ├── testing │ ├── err_reader.go │ └── context_test.go ├── utils │ ├── fs │ │ ├── check.go │ │ ├── discover.go │ │ └── check_test.go │ ├── flag │ │ └── flag.go │ └── maps │ │ └── merge.go ├── validation │ ├── test │ │ ├── test.go │ │ ├── test_step.go │ │ ├── file_ref.go │ │ ├── check.go │ │ ├── assert.go │ │ ├── error.go │ │ ├── events.go │ │ ├── get.go │ │ ├── apply.go │ │ ├── create.go │ │ ├── describe.go │ │ ├── patch.go │ │ ├── update.go │ │ ├── object_reference.go │ │ ├── delete.go │ │ ├── expectations.go │ │ ├── output.go │ │ ├── file_ref_or_check.go │ │ ├── wait.go │ │ ├── pod_logs.go │ │ ├── file_ref_or_resource.go │ │ ├── binding.go │ │ ├── script.go │ │ ├── command.go │ │ ├── test_spec.go │ │ ├── test_spec_test.go │ │ ├── test_step_spec.go │ │ ├── resource_reference.go │ │ └── check_test.go │ └── config │ │ ├── cluster.go │ │ ├── configuration.go │ │ ├── configuration_spec.go │ │ └── cluster_test.go ├── mutate │ ├── mutation.go │ └── mutate.go ├── internal │ └── loader │ │ ├── testing │ │ └── fake_loader.go │ │ └── default.go └── values │ └── load.go ├── website ├── docs │ ├── blog │ │ ├── index.md │ │ ├── img │ │ │ └── chainsaw.png │ │ └── .authors.yml │ ├── index.md │ ├── static │ │ ├── favicon.ico │ │ ├── extra.css │ │ ├── kyverno-chainsaw-logo.png │ │ └── kyverno-chainsaw-horizontal.png │ ├── commands │ │ ├── chainsaw_version.md │ │ ├── chainsaw_export_schemas.md │ │ ├── chainsaw_build.md │ │ ├── chainsaw_export.md │ │ ├── chainsaw_create.md │ │ ├── chainsaw_migrate.md │ │ ├── chainsaw_migrate_kuttl_tests.md │ │ ├── chainsaw_migrate_kuttl_config.md │ │ ├── chainsaw_docs.md │ │ ├── chainsaw_create_test.md │ │ ├── chainsaw_lint.md │ │ ├── chainsaw_migrate_kuttl.md │ │ ├── chainsaw_build_docs.md │ │ ├── chainsaw_completion_powershell.md │ │ ├── chainsaw_completion_fish.md │ │ ├── chainsaw_completion.md │ │ └── chainsaw.md │ ├── docs.md │ ├── more │ │ ├── events.md │ │ └── lint.md │ ├── configuration │ │ ├── no-cluster.md │ │ ├── selector.md │ │ ├── reports.md │ │ ├── cleanup-delay.md │ │ ├── grace.md │ │ └── values.md │ ├── collectors │ │ └── index.md │ ├── operations │ │ ├── sleep.md │ │ └── non-resource-assert.md │ ├── resources.md │ └── examples │ │ ├── non-resource-assertion.md │ │ └── inline.md └── apis │ └── markdown │ └── type.tpl ├── CODEOWNERS ├── .ko.yaml ├── .github ├── cherry-pick-bot.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── codegen.yaml │ └── lint.yaml ├── .assets ├── kyverno-chainsaw-logo.png ├── kyverno-chainsaw-logo.pptx └── kyverno-chainsaw-horizontal.png ├── .gitignore ├── .release-notes ├── v0.0.5.md ├── v0.1.6.md ├── _template.md ├── v0.0.1.md ├── v0.0.3.md ├── v0.1.0.md ├── v0.1.2.md ├── main.md ├── v0.0.4.md ├── v0.1.3.md └── v0.2.0.md ├── codecov.yml ├── main.go ├── .hack └── boilerplate.go.txt ├── MAINTAINERS.md └── .golangci.yml /testdata/config/empty.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/resource/empty.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/test/empty.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/values/empty.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/apis/doc.go: -------------------------------------------------------------------------------- 1 | package apis 2 | -------------------------------------------------------------------------------- /testdata/e2e/values.yaml: -------------------------------------------------------------------------------- 1 | foo: bar -------------------------------------------------------------------------------- /testdata/commands/docs/invalid-output.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/discovery/empty-test/fake.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/values/invalid.yaml: -------------------------------------------------------------------------------- 1 | this, or that -------------------------------------------------------------------------------- /website/docs/blog/index.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | 3 | -------------------------------------------------------------------------------- /testdata/commands/migrate/kuttl/config/out-save.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/commands/migrate/kuttl/tests/out-save.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kyverno/chainsaw-maintainers 2 | -------------------------------------------------------------------------------- /testdata/resource/invalid.yaml: -------------------------------------------------------------------------------- 1 | this: is 2 | a: invalid 3 | -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | defaultBaseImage: cgr.dev/chainguard/kubectl:latest-dev -------------------------------------------------------------------------------- /testdata/values/values-1.yaml: -------------------------------------------------------------------------------- 1 | foo: 2 | bar: baz 3 | test: 42 -------------------------------------------------------------------------------- /testdata/values/values-2.yaml: -------------------------------------------------------------------------------- 1 | foo: 2 | bar: baz 3 | test: ~ -------------------------------------------------------------------------------- /testdata/resource/folder-invalid/invalid.yaml: -------------------------------------------------------------------------------- 1 | this: is 2 | a: invalid 3 | -------------------------------------------------------------------------------- /.github/cherry-pick-bot.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | preservePullRequestTitle: true 3 | -------------------------------------------------------------------------------- /website/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: home.html 3 | title: chainsaw 4 | --- 5 | -------------------------------------------------------------------------------- /testdata/commands/lint/test/pass.txt: -------------------------------------------------------------------------------- 1 | Processing input... 2 | The document is valid 3 | -------------------------------------------------------------------------------- /testdata/commands/version/out.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Time: --- 3 | Git commit ID: --- 4 | -------------------------------------------------------------------------------- /testdata/commands/lint/configuration/pass.txt: -------------------------------------------------------------------------------- 1 | Processing input... 2 | The document is valid 3 | -------------------------------------------------------------------------------- /testdata/kuttl/invalid-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestSuite 3 | data: 4 | foo: bar 5 | -------------------------------------------------------------------------------- /website/docs/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/website/docs/static/favicon.ico -------------------------------------------------------------------------------- /.assets/kyverno-chainsaw-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/.assets/kyverno-chainsaw-logo.png -------------------------------------------------------------------------------- /.assets/kyverno-chainsaw-logo.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/.assets/kyverno-chainsaw-logo.pptx -------------------------------------------------------------------------------- /testdata/config/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: default 5 | data: 6 | foo: bar -------------------------------------------------------------------------------- /testdata/e2e/examples/non-resource-assertion/assert.yaml: -------------------------------------------------------------------------------- 1 | (x_k8s_list($client, 'v1', 'Node')): 2 | (length(items)): 1 3 | -------------------------------------------------------------------------------- /testdata/e2e/examples/non-resource-assertion/error.yaml: -------------------------------------------------------------------------------- 1 | (x_k8s_list($client, 'v1', 'Node')): 2 | (length(items)): 2 3 | -------------------------------------------------------------------------------- /testdata/test/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: default 5 | data: 6 | foo: bar -------------------------------------------------------------------------------- /testdata/test/no-spec.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test-1 5 | -------------------------------------------------------------------------------- /website/docs/blog/img/chainsaw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/website/docs/blog/img/chainsaw.png -------------------------------------------------------------------------------- /pkg/runner/logging/t_logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | type TLogger interface { 4 | Log(args ...any) 5 | Helper() 6 | } 7 | -------------------------------------------------------------------------------- /testdata/test/no-steps.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test-1 5 | spec: {} -------------------------------------------------------------------------------- /website/docs/static/extra.css: -------------------------------------------------------------------------------- 1 | body > header > nav > a > img { 2 | border-radius: 10%; 3 | border: 1px solid #555; 4 | } -------------------------------------------------------------------------------- /.assets/kyverno-chainsaw-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/.assets/kyverno-chainsaw-horizontal.png -------------------------------------------------------------------------------- /testdata/kuttl/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /testdata/e2e/examples/deployment/assertions.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: nginx 6 | spec: {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gopath/ 2 | .temp/ 3 | .tools/ 4 | chainsaw 5 | coverage.out 6 | website/site 7 | pkg/runner/chainsaw.xml 8 | pkg/runner/.json 9 | -------------------------------------------------------------------------------- /testdata/config/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: default 5 | spec: {} -------------------------------------------------------------------------------- /testdata/kuttl/kuttl-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestSuite 3 | parallel: 4 4 | timeout: 300 5 | startControlPlane: true -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/match.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Match represents a match condition against an evaluated object. 4 | type Match = Any 5 | -------------------------------------------------------------------------------- /pkg/data/config/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: default 5 | spec: {} 6 | -------------------------------------------------------------------------------- /testdata/commands/test/wrong_kind_config.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading config (../../../testdata/commands/test/config/wrong_kind_config.yaml)... 3 | -------------------------------------------------------------------------------- /testdata/e2e/examples/basic/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /website/docs/static/kyverno-chainsaw-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/website/docs/static/kyverno-chainsaw-logo.png -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/check.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Check represents a check to be applied on the result of an operation. 4 | type Check = Any 5 | -------------------------------------------------------------------------------- /pkg/runner/operations/internal/interval.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const PollInterval = 50 * time.Millisecond 8 | -------------------------------------------------------------------------------- /testdata/commands/assert/assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /testdata/commands/test/wrong_format_config.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading config (../../../testdata/commands/test/config/wrong_format_config.yaml)... 3 | -------------------------------------------------------------------------------- /testdata/e2e/examples/basic/configmap-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /.release-notes/v0.0.5.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.0.5`. 4 | 5 | ## 💫 New features 💫 6 | 7 | - Support new assertion tree model :tada: -------------------------------------------------------------------------------- /pkg/client/auth.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | _ "k8s.io/client-go/plugin/pkg/client/auth" // package needed for auth providers like GCP 5 | ) 6 | -------------------------------------------------------------------------------- /testdata/commands/root/help.txt: -------------------------------------------------------------------------------- 1 | Stronger tool for e2e testing 2 | 3 | Usage: 4 | chainsaw [flags] 5 | 6 | Flags: 7 | -h, --help help for chainsaw 8 | -------------------------------------------------------------------------------- /testdata/discovery/manifests/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: chainsaw-quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /testdata/discovery/manifests/01-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: chainsaw-quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /testdata/discovery/manifests/01-errors.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: chainsaw-quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /testdata/discovery/not-a-test/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: chainsaw-quick-start 5 | data: 6 | foo: bar 7 | -------------------------------------------------------------------------------- /testdata/test/bad-step.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test-1 5 | spec: 6 | steps: 7 | - foo: bar -------------------------------------------------------------------------------- /website/docs/static/kyverno-chainsaw-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/chainsaw/main/website/docs/static/kyverno-chainsaw-horizontal.png -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/wait_deletion.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Deletion represents parameters for waiting on a resource's deletion. 4 | type Deletion struct{} 5 | -------------------------------------------------------------------------------- /testdata/commands/export/schemas/help.txt: -------------------------------------------------------------------------------- 1 | Export JSON schemas 2 | 3 | Usage: 4 | chainsaw schemas [flags] 5 | 6 | Flags: 7 | -h, --help help for schemas 8 | -------------------------------------------------------------------------------- /testdata/commands/version/help.txt: -------------------------------------------------------------------------------- 1 | Print the version informations 2 | 3 | Usage: 4 | chainsaw version [flags] 5 | 6 | Flags: 7 | -h, --help help for version 8 | -------------------------------------------------------------------------------- /testdata/commands/test/config/wrong_kind_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: foo 3 | metadata: 4 | name: default 5 | namespace: kyverno 6 | -------------------------------------------------------------------------------- /.release-notes/v0.1.6.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.1.6`. 4 | 5 | ## 🔧 Fixes 🔧 6 | 7 | - Fix an issue with temporary KUBECONFIG not being an absolute path -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - ^pkg/apis/v1alpha1/zz_generated\.deepcopy\.go$ 3 | - ^pkg/apis/v1alpha1/zz_generated\.register\.go$ 4 | - ^pkg/testing/.*\.go$ 5 | - ^.*_test\.go$ 6 | -------------------------------------------------------------------------------- /pkg/commands/build/docs/catalog.tmpl: -------------------------------------------------------------------------------- 1 | # Tests catalog 2 | {{ range .Tests }} 3 | - [{{ .ObjectMeta.Name }}]({{ fpRel $.BasePath (fpJoin .BasePath $.Readme) }}) 4 | {{- end }} 5 | -------------------------------------------------------------------------------- /testdata/commands/test/config/empty_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: default 5 | namespace: kyverno 6 | -------------------------------------------------------------------------------- /testdata/kuttl/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestAssert 3 | timeout: 30 4 | commands: 5 | - command: echo hello 6 | collectors: 7 | - type: pod 8 | pod: nginx -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/any.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "github.com/kyverno/kyverno-json/pkg/apis/policy/v1alpha1" 5 | ) 6 | 7 | // Any represents any type. 8 | type Any = v1alpha1.Any 9 | -------------------------------------------------------------------------------- /testdata/kuttl/02-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestAssert 3 | timeout: 30 4 | commands: 5 | - command: echo hello 6 | collectors: 7 | - type: command 8 | command: sleep 1 -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/format.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Format determines the output format (json or yaml). 4 | // +kubebuilder:validation:Pattern=`^(?:json|yaml|\(.+\))$` 5 | type Format string 6 | -------------------------------------------------------------------------------- /testdata/resource/valid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: test-pod 6 | --- 7 | apiVersion: v1 8 | kind: Service 9 | metadata: 10 | name: test-service 11 | -------------------------------------------------------------------------------- /testdata/commands/test/config/wrong_format_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: default 5 | namespace: kyverno 6 | spec: 7 | foo: bar 8 | -------------------------------------------------------------------------------- /testdata/commands/test/wrong_kind_config_err.txt: -------------------------------------------------------------------------------- 1 | Error: failed to load configuration (failed to parse document (failed to retrieve validator: kind foo not found in chainsaw.kyverno.io/v1alpha1 groupversion)) 2 | -------------------------------------------------------------------------------- /testdata/resource/folder-valid/valid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: test-pod 6 | --- 7 | apiVersion: v1 8 | kind: Service 9 | metadata: 10 | name: test-service 11 | -------------------------------------------------------------------------------- /pkg/discovery/test.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | ) 6 | 7 | type Test struct { 8 | *v1alpha1.Test 9 | BasePath string 10 | Err error 11 | } 12 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group. 2 | // +k8s:deepcopy-gen=package 3 | // +kubebuilder:object:generate=true 4 | // +groupName=chainsaw.kyverno.io 5 | package v1alpha1 6 | -------------------------------------------------------------------------------- /pkg/runner/cleanup/skip.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | func Skip(config bool, test *bool, step *bool) bool { 4 | if step != nil { 5 | return *step 6 | } 7 | if test != nil { 8 | return *test 9 | } 10 | return config 11 | } 12 | -------------------------------------------------------------------------------- /pkg/client/pet.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | petname "github.com/dustinkirkland/golang-petname" 7 | ) 8 | 9 | func Pet() string { 10 | return fmt.Sprintf("chainsaw-%s", petname.Generate(2, "-")) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/runner/cleanup/cleaner.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/client" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | ) 7 | 8 | type Cleaner = func(unstructured.Unstructured, client.Client) 9 | -------------------------------------------------------------------------------- /pkg/runner/processors/info.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | type TestInfo struct { 4 | Id int 5 | } 6 | 7 | type StepInfo struct { 8 | Id int 9 | } 10 | 11 | type OperationInfo struct { 12 | Id int 13 | ResourceId int 14 | } 15 | -------------------------------------------------------------------------------- /pkg/testing/err_reader.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type ErrReader struct{} 8 | 9 | func (e *ErrReader) Read(p []byte) (n int, err error) { 10 | return 0, errors.New("error reading from stdin") 11 | } 12 | -------------------------------------------------------------------------------- /pkg/runner/functions/functions_test.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetFunctions(t *testing.T) { 10 | assert.Equal(t, 6, len(GetFunctions())) 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /testdata/e2e/examples/apply-outputs/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: first 5 | data: 6 | foo: bar 7 | --- 8 | apiVersion: v1 9 | kind: ConfigMap 10 | metadata: 11 | name: second 12 | data: 13 | foo: bar 14 | -------------------------------------------------------------------------------- /pkg/utils/fs/check.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func CheckFolders(paths ...string) error { 8 | for _, path := range paths { 9 | if _, err := os.Stat(path); err != nil { 10 | return err 11 | } 12 | } 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/runner/template/template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | const DefaultTemplate = true 4 | 5 | func Get(values ...*bool) bool { 6 | for _, value := range values { 7 | if value != nil { 8 | return *value 9 | } 10 | } 11 | return DefaultTemplate 12 | } 13 | -------------------------------------------------------------------------------- /testdata/kuttl/multiple-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestSuite 3 | parallel: 4 4 | timeout: 300 5 | startControlPlane: true 6 | --- 7 | apiVersion: kuttl.dev/v1beta1 8 | kind: TestSuite 9 | parallel: 4 10 | timeout: 300 11 | startControlPlane: true 12 | -------------------------------------------------------------------------------- /testdata/test/multiple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test-1 5 | spec: 6 | steps: [] 7 | --- 8 | apiVersion: chainsaw.kyverno.io/v1alpha1 9 | kind: Test 10 | metadata: 11 | name: test-2 12 | spec: 13 | steps: [] 14 | -------------------------------------------------------------------------------- /pkg/runner/functions/env.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func jpEnv(arguments []any) (any, error) { 8 | var key string 9 | if err := getArg(arguments, 0, &key); err != nil { 10 | return nil, err 11 | } 12 | return os.Getenv(key), nil 13 | } 14 | -------------------------------------------------------------------------------- /testdata/commands/migrate/kuttl/tests/help.txt: -------------------------------------------------------------------------------- 1 | Migrate KUTTL tests to Chainsaw 2 | 3 | Usage: 4 | chainsaw tests [flags] 5 | 6 | Flags: 7 | --cleanup If set, delete converted files 8 | -h, --help help for tests 9 | --save If set, converted files will be saved 10 | -------------------------------------------------------------------------------- /testdata/commands/migrate/kuttl/config/help.txt: -------------------------------------------------------------------------------- 1 | Migrate KUTTL config to Chainsaw 2 | 3 | Usage: 4 | chainsaw config [flags] 5 | 6 | Flags: 7 | --cleanup If set, delete converted files 8 | -h, --help help for config 9 | --save If set, converted files will be saved 10 | -------------------------------------------------------------------------------- /testdata/validation/example-file.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx 5 | labels: 6 | name: nginx 7 | spec: 8 | containers: 9 | - name: nginx 10 | image: nginx 11 | resources: 12 | limits: 13 | memory: "128Mi" 14 | cpu: "500m" 15 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha2/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha2 contains API Schema definitions for the v1alpha2 API group. 2 | // +k8s:deepcopy-gen=package 3 | // +k8s:conversion-gen=github.com/kyverno/chainsaw/pkg/apis/v1alpha1 4 | // +kubebuilder:object:generate=true 5 | // +groupName=chainsaw.kyverno.io 6 | package v1alpha2 7 | -------------------------------------------------------------------------------- /testdata/config/multiple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: default 5 | spec: {} 6 | --- 7 | apiVersion: chainsaw.kyverno.io/v1alpha1 8 | kind: Configuration 9 | metadata: 10 | name: timeout-1m 11 | spec: 12 | timeouts: 13 | assert: 1m 14 | -------------------------------------------------------------------------------- /testdata/runner/processors/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: myapp 5 | labels: 6 | name: myapp 7 | spec: 8 | containers: 9 | - name: myapp 10 | image: myapp:latest 11 | resources: 12 | limits: 13 | memory: "128Mi" 14 | cpu: "500m" 15 | -------------------------------------------------------------------------------- /pkg/runner/operations/operation.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmespath-community/go-jmespath/pkg/binding" 7 | ) 8 | 9 | type Outputs = map[string]any 10 | 11 | type Operation interface { 12 | Exec(context.Context, binding.Bindings) (Outputs, error) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/sleep.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // Sleep represents a duration while nothing happens. 8 | type Sleep struct { 9 | // Duration is the delay used for sleeping. 10 | Duration metav1.Duration `json:"duration"` 11 | } 12 | -------------------------------------------------------------------------------- /.release-notes/_template.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `TODO`. 4 | 5 | 22 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/test_spec_step.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // TestStep contains the test step definition used in a test spec. 4 | type TestStep struct { 5 | // Name of the step. 6 | // +optional 7 | Name string `json:"name,omitempty"` 8 | 9 | // TestStepSpec of the step. 10 | TestStepSpec `json:",inline"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/runner/timeout/timeout.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "time" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | func Get(operation *metav1.Duration, fallback time.Duration) *time.Duration { 10 | if operation != nil { 11 | return &operation.Duration 12 | } 13 | return &fallback 14 | } 15 | -------------------------------------------------------------------------------- /website/docs/blog/.authors.yml: -------------------------------------------------------------------------------- 1 | authors: 2 | eddycharly: 3 | name: Charles-Edouard Brétéché 4 | description: Creator 5 | avatar: https://avatars.githubusercontent.com/u/47974576 6 | shubham-cmyk: 7 | name: Shubham Gupta 8 | description: Contributor 9 | avatar: https://avatars.githubusercontent.com/u/69793468 -------------------------------------------------------------------------------- /testdata/runner/processors/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: chainsaw 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | editor: vscode 11 | spec: 12 | containers: 13 | - name: nginx 14 | image: nginx:1.7.9 -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_version.md: -------------------------------------------------------------------------------- 1 | ## chainsaw version 2 | 3 | Print the version informations 4 | 5 | ``` 6 | chainsaw version [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for version 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 18 | 19 | -------------------------------------------------------------------------------- /testdata/commands/build/help.txt: -------------------------------------------------------------------------------- 1 | Build commands 2 | 3 | Usage: 4 | chainsaw build [flags] 5 | chainsaw build [command] 6 | 7 | Available Commands: 8 | docs Build tests documentation 9 | 10 | Flags: 11 | -h, --help help for build 12 | 13 | Use "chainsaw build [command] --help" for more information about a command. 14 | -------------------------------------------------------------------------------- /testdata/commands/export/help.txt: -------------------------------------------------------------------------------- 1 | Export commands 2 | 3 | Usage: 4 | chainsaw export [flags] 5 | chainsaw export [command] 6 | 7 | Available Commands: 8 | schemas Export JSON schemas 9 | 10 | Flags: 11 | -h, --help help for export 12 | 13 | Use "chainsaw export [command] --help" for more information about a command. 14 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_export_schemas.md: -------------------------------------------------------------------------------- 1 | ## chainsaw export schemas 2 | 3 | Export JSON schemas 4 | 5 | ``` 6 | chainsaw export schemas [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for schemas 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw export](chainsaw_export.md) - Export commands 18 | 19 | -------------------------------------------------------------------------------- /testdata/commands/create/help.txt: -------------------------------------------------------------------------------- 1 | Create Chainsaw resources 2 | 3 | Usage: 4 | chainsaw create [flags] 5 | chainsaw create [command] 6 | 7 | Available Commands: 8 | test Create a Chainsaw test 9 | 10 | Flags: 11 | -h, --help help for create 12 | 13 | Use "chainsaw create [command] --help" for more information about a command. 14 | -------------------------------------------------------------------------------- /testdata/discovery/test-yml/chainsaw-test.yml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test 5 | spec: 6 | steps: 7 | - name: create configmap 8 | try: 9 | - apply: 10 | file: configmap.yaml 11 | - name: assert configmap 12 | try: 13 | - assert: 14 | file: configmap.yaml 15 | -------------------------------------------------------------------------------- /testdata/discovery/test/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test 5 | spec: 6 | steps: 7 | - name: create configmap 8 | try: 9 | - apply: 10 | file: configmap.yaml 11 | - name: assert configmap 12 | try: 13 | - assert: 14 | file: configmap.yaml 15 | -------------------------------------------------------------------------------- /testdata/resource/list.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: cm-1 8 | namespace: default 9 | data: 10 | key: 'yes' 11 | - apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: cm-2 15 | namespace: default 16 | data: 17 | key: 'no' 18 | -------------------------------------------------------------------------------- /website/docs/docs.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Starting with Chainsaw `v0.0.6` we maintain separate docs per version: 4 | 5 | - Every version released has its own documentation 6 | - See [documentation for latest released version](https://kyverno.github.io/chainsaw/latest/writing-tests/) 7 | - Current work in progress (`main` branch) is also available 8 | -------------------------------------------------------------------------------- /.release-notes/v0.0.1.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.0.1`. 4 | 5 | ## 💫 New features 💫 6 | 7 | - Initial MVP release with support for `Apply`, `Delete`, `Assert` and `Error` operations 8 | - Supports configuration from both config file and command line flags 9 | - Full documentation available at https://kyverno.github.io/chainsaw 10 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/cluster.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Cluster defines cluster config and context. 4 | type Cluster struct { 5 | // Kubeconfig is the path to the referenced file. 6 | Kubeconfig string `json:"kubeconfig"` 7 | 8 | // Context is the name of the context to use. 9 | // +optional 10 | Context string `json:"context,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /testdata/e2e/examples/list/list.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: cm-1 8 | namespace: default 9 | data: 10 | key: 'yes' 11 | - apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: cm-2 15 | namespace: default 16 | data: 17 | key: 'no' 18 | -------------------------------------------------------------------------------- /website/docs/more/events.md: -------------------------------------------------------------------------------- 1 | # Working with events 2 | 3 | Kubernetes events are regular Kubernetes objects and can be asserted on just like any other object: 4 | 5 | ```yaml 6 | apiVersion: v1 7 | kind: Event 8 | reason: Started 9 | source: 10 | component: kubelet 11 | involvedObject: 12 | apiVersion: v1 13 | kind: Pod 14 | name: my-pod 15 | ``` 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-logr/logr" 7 | "github.com/kyverno/chainsaw/pkg/commands" 8 | "sigs.k8s.io/controller-runtime/pkg/log" 9 | ) 10 | 11 | func main() { 12 | log.SetLogger(logr.Discard()) 13 | root := commands.RootCommand() 14 | if err := root.Execute(); err != nil { 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/runner/logging/testing/fake_t_logger.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type FakeTLogger struct { 8 | Messages []string 9 | } 10 | 11 | func (tl *FakeTLogger) Log(args ...any) { 12 | for _, arg := range args { 13 | tl.Messages = append(tl.Messages, fmt.Sprint(arg)) 14 | } 15 | } 16 | 17 | func (tl *FakeTLogger) Helper() {} 18 | -------------------------------------------------------------------------------- /testdata/e2e/examples/deployment/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: example 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: nginx 9 | template: 10 | metadata: 11 | labels: 12 | app: nginx 13 | spec: 14 | containers: 15 | - name: nginx 16 | image: nginx:1.14.2 17 | -------------------------------------------------------------------------------- /testdata/commands/docs/help.txt: -------------------------------------------------------------------------------- 1 | Generate reference documentation 2 | 3 | Usage: 4 | chainsaw docs [flags] 5 | 6 | Flags: 7 | --autogenTag Determines if the generated docs should contain a timestamp (default true) 8 | -h, --help help for docs 9 | -o, --output string Output path (default ".") 10 | --website Website version 11 | -------------------------------------------------------------------------------- /testdata/commands/migrate/help.txt: -------------------------------------------------------------------------------- 1 | Migrate resources to Chainsaw 2 | 3 | Usage: 4 | chainsaw migrate [flags] 5 | chainsaw migrate [command] 6 | 7 | Available Commands: 8 | kuttl Migrate KUTTL resources to Chainsaw 9 | 10 | Flags: 11 | -h, --help help for migrate 12 | 13 | Use "chainsaw migrate [command] --help" for more information about a command. 14 | -------------------------------------------------------------------------------- /testdata/e2e/examples/assertion-tree/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: assertion-tree 6 | spec: 7 | steps: 8 | - try: 9 | - assert: 10 | file: assert.yaml 11 | -------------------------------------------------------------------------------- /pkg/runner/operations/internal/namespace.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/runner/namespacer" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | ) 7 | 8 | func ApplyNamespacer(namespacer namespacer.Namespacer, obj client.Object) error { 9 | if namespacer == nil { 10 | return nil 11 | } 12 | return namespacer.Apply(obj) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/testing/context_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | assert.Nil(t, FromContext(context.Background())) 12 | assert.NotNil(t, IntoContext(context.Background(), t)) 13 | assert.Equal(t, t, FromContext(IntoContext(context.Background(), t))) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/utils/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | ) 6 | 7 | // IsSet returns true if a flag is set on the command line. 8 | func IsSet(flagSet *pflag.FlagSet, name string) bool { 9 | found := false 10 | flagSet.Visit(func(flag *pflag.Flag) { 11 | if flag.Name == name { 12 | found = true 13 | } 14 | }) 15 | return found 16 | } 17 | -------------------------------------------------------------------------------- /testdata/commands/create/test/help.txt: -------------------------------------------------------------------------------- 1 | Create a Chainsaw test 2 | 3 | Usage: 4 | chainsaw test [flags] 5 | 6 | Flags: 7 | --description If set, adds description when applicable (default true) 8 | --force If set, existing test will be deleted if needed 9 | -h, --help help for test 10 | --save If set, created test will be saved 11 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/wait_jsonpath.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // JsonPath represents parameters for waiting on a json path of a resource. 4 | type JsonPath struct { 5 | // Path defines the json path to wait for, e.g. '{.status.phase}'. 6 | Path string `json:"path"` 7 | 8 | // Value defines the expected value to wait for, e.g., "Running". 9 | Value string `json:"value"` 10 | } 11 | -------------------------------------------------------------------------------- /testdata/e2e/config.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/configuration-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Configuration 4 | metadata: 5 | name: configuration 6 | spec: 7 | fullName: true 8 | failFast: true 9 | forceTerminationGracePeriod: 5s 10 | parallel: 1 11 | -------------------------------------------------------------------------------- /testdata/e2e/examples/values/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: values 6 | spec: 7 | steps: 8 | - try: 9 | - assert: 10 | resource: 11 | ($values.foo): bar 12 | -------------------------------------------------------------------------------- /pkg/commands/root/command.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func Command() *cobra.Command { 8 | return &cobra.Command{ 9 | Use: "chainsaw", 10 | Short: "Stronger tool for e2e testing", 11 | SilenceUsage: true, 12 | RunE: func(cmd *cobra.Command, _ []string) error { 13 | return cmd.Help() 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/runner/logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/kyverno/pkg/ext/output/color" 8 | ) 9 | 10 | func Log(ctx context.Context, operation Operation, status Status, color *color.Color, args ...fmt.Stringer) { 11 | logger := FromContext(ctx) 12 | if logger != nil { 13 | logger.Log(operation, status, color, args...) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/file_ref.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // FileRef represents a file reference. 4 | type FileRef struct { 5 | // File is the path to the referenced file. This can be a direct path to a file 6 | // or an expression that matches multiple files, such as "manifest/*.yaml" for all YAML 7 | // files within the "manifest" directory. 8 | File string `json:"file,omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /pkg/validation/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateTest(obj *v1alpha1.Test) field.ErrorList { 9 | var errs field.ErrorList 10 | var path *field.Path 11 | errs = append(errs, ValidateTestSpec(path.Child("spec"), obj.Spec)...) 12 | return errs 13 | } 14 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_build.md: -------------------------------------------------------------------------------- 1 | ## chainsaw build 2 | 3 | Build commands 4 | 5 | ``` 6 | chainsaw build [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for build 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 18 | * [chainsaw build docs](chainsaw_build_docs.md) - Build tests documentation 19 | 20 | -------------------------------------------------------------------------------- /pkg/mutate/mutation.go: -------------------------------------------------------------------------------- 1 | package mutate 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmespath-community/go-jmespath/pkg/binding" 7 | "github.com/kyverno/kyverno-json/pkg/engine/template" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | type Mutation interface { 12 | mutate(context.Context, *field.Path, any, binding.Bindings, ...template.Option) (any, error) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/runner/logging/testing/logger.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type ( 11 | Operation string 12 | Status string 13 | ) 14 | 15 | type Logger interface { 16 | Log(Operation, Status, *color.Color, ...fmt.Stringer) 17 | WithResource(ctrlclient.Object) Logger 18 | } 19 | -------------------------------------------------------------------------------- /pkg/validation/test/test_step.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateTestStep(path *field.Path, obj v1alpha1.TestStep) field.ErrorList { 9 | var errs field.ErrorList 10 | errs = append(errs, ValidateTestStepSpec(path, obj.TestStepSpec)...) 11 | return errs 12 | } 13 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_export.md: -------------------------------------------------------------------------------- 1 | ## chainsaw export 2 | 3 | Export commands 4 | 5 | ``` 6 | chainsaw export [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for export 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 18 | * [chainsaw export schemas](chainsaw_export_schemas.md) - Export JSON schemas 19 | 20 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/output.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Output represents an output binding with a match to determine if the binding must be considered or not. 4 | type Output struct { 5 | // Binding determines the binding to create when the match succeeds. 6 | Binding `json:",inline"` 7 | 8 | // Match defines the matching statement. 9 | // +optional 10 | Match *Match `json:"match,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_create.md: -------------------------------------------------------------------------------- 1 | ## chainsaw create 2 | 3 | Create Chainsaw resources 4 | 5 | ``` 6 | chainsaw create [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for create 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 18 | * [chainsaw create test](chainsaw_create_test.md) - Create a Chainsaw test 19 | 20 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/object_type.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // ObjectType represents a specific apiVersion and kind. 4 | type ObjectType struct { 5 | // API version of the referent. 6 | APIVersion string `json:"apiVersion"` 7 | 8 | // Kind of the referent. 9 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 10 | Kind string `json:"kind"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/client/unstructured.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | ) 7 | 8 | func ToUnstructured(obj any) unstructured.Unstructured { 9 | data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 10 | if err != nil { 11 | panic(err) 12 | } 13 | return unstructured.Unstructured{Object: data} 14 | } 15 | -------------------------------------------------------------------------------- /testdata/e2e/examples/assertion-tree/assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | k8s-app: kube-dns 6 | namespace: kube-system 7 | spec: 8 | template: 9 | (spec)->specBinding: 10 | # the ~ modifier tells Chainsaw to iterate over the array elements 11 | ~.(containers): 12 | ($specBinding.securityContext != null || securityContext != null): true 13 | -------------------------------------------------------------------------------- /testdata/e2e/examples/deployment/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: deployment 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | file: resources.yaml 11 | - assert: 12 | file: assertions.yaml 13 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/file_ref_or_check.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // FileRefOrCheck represents a file reference or resource. 4 | type FileRefOrCheck struct { 5 | // FileRef provides a reference to the file containing the resources to be applied. 6 | // +optional 7 | FileRef `json:",inline"` 8 | 9 | // Check provides a check used in assertions. 10 | // +optional 11 | Check *Check `json:"resource,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /testdata/commands/migrate/kuttl/help.txt: -------------------------------------------------------------------------------- 1 | Migrate KUTTL resources to Chainsaw 2 | 3 | Usage: 4 | chainsaw kuttl [flags] 5 | chainsaw kuttl [command] 6 | 7 | Available Commands: 8 | config Migrate KUTTL config to Chainsaw 9 | tests Migrate KUTTL tests to Chainsaw 10 | 11 | Flags: 12 | -h, --help help for kuttl 13 | 14 | Use "chainsaw kuttl [command] --help" for more information about a command. 15 | -------------------------------------------------------------------------------- /testdata/e2e/examples/array-assertion/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: array-assertions 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | file: resources.yaml 11 | - assert: 12 | file: assertions.yaml 13 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_migrate.md: -------------------------------------------------------------------------------- 1 | ## chainsaw migrate 2 | 3 | Migrate resources to Chainsaw 4 | 5 | ``` 6 | chainsaw migrate [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for migrate 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 18 | * [chainsaw migrate kuttl](chainsaw_migrate_kuttl.md) - Migrate KUTTL resources to Chainsaw 19 | 20 | -------------------------------------------------------------------------------- /testdata/e2e/examples/timeout/chainsaw-test.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: timeout 6 | spec: 7 | steps: 8 | - try: 9 | - script: 10 | content: sleep 5 11 | timeout: 3s 12 | check: 13 | ($error != null): true 14 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/expectation.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Expectation represents a check to be applied on the result of an operation 4 | // with a match filter to determine if the verification should be considered. 5 | type Expectation struct { 6 | // Match defines the matching statement. 7 | // +optional 8 | Match *Match `json:"match,omitempty"` 9 | 10 | // Check defines the verification statement. 11 | Check Check `json:"check"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/wait_condition.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Condition represents parameters for waiting on a specific condition of a resource. 4 | type Condition struct { 5 | // Name defines the specific condition to wait for, e.g., "Available", "Ready". 6 | Name string `json:"name"` 7 | 8 | // Value defines the specific condition status to wait for, e.g., "True", "False". 9 | // +optional 10 | Value *string `json:"value,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/runner/failer/context.go: -------------------------------------------------------------------------------- 1 | package failer 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey struct{} 8 | 9 | func FromContext(ctx context.Context) Failer { 10 | if ctx != nil { 11 | if v, ok := ctx.Value(contextKey{}).(Failer); ok { 12 | return v 13 | } 14 | } 15 | return nil 16 | } 17 | 18 | func IntoContext(ctx context.Context, failer Failer) context.Context { 19 | return context.WithValue(ctx, contextKey{}, failer) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/runner/logging/context.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey struct{} 8 | 9 | func FromContext(ctx context.Context) Logger { 10 | if ctx != nil { 11 | if v, ok := ctx.Value(contextKey{}).(Logger); ok { 12 | return v 13 | } 14 | } 15 | return nil 16 | } 17 | 18 | func IntoContext(ctx context.Context, logger Logger) context.Context { 19 | return context.WithValue(ctx, contextKey{}, logger) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/validation/config/cluster.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateCluster(path *field.Path, obj v1alpha1.Cluster) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj.Kubeconfig == "" { 11 | errs = append(errs, field.Required(path.Child("kubeconfig"), "a kubeconfig is required")) 12 | } 13 | return errs 14 | } 15 | -------------------------------------------------------------------------------- /pkg/validation/test/file_ref.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateFileRef(path *field.Path, obj v1alpha1.FileRef) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj.File == "" { 11 | errs = append(errs, field.Invalid(path.Child("file"), obj, "a file reference must be specified")) 12 | } 13 | return errs 14 | } 15 | -------------------------------------------------------------------------------- /testdata/runner/processors/cron-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: hello 5 | namespace: default 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: hello 14 | image: busybox 15 | args: ['/bin/sh', '-c', 'date; echo Hello from the Chainsaw'] 16 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /pkg/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | ) 7 | 8 | const CrdsFolder = "crds" 9 | 10 | //go:embed crds 11 | var crdsFs embed.FS 12 | 13 | //go:embed config 14 | var configFs embed.FS 15 | 16 | //go:embed schemas 17 | var schemasFs embed.FS 18 | 19 | func Crds() fs.FS { 20 | return crdsFs 21 | } 22 | 23 | func Config() fs.FS { 24 | return configFs 25 | } 26 | 27 | func Schemas() fs.FS { 28 | return schemasFs 29 | } 30 | -------------------------------------------------------------------------------- /pkg/validation/test/check.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateCheck(path *field.Path, obj *v1alpha1.Check) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Value == nil { 12 | errs = append(errs, field.Invalid(path, obj, "a value must be specified")) 13 | } 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/maps/merge.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | func Merge(a, b map[string]any) map[string]any { 4 | out := make(map[string]any, len(a)) 5 | for k, v := range a { 6 | out[k] = v 7 | } 8 | for k, v := range b { 9 | if v, ok := v.(map[string]any); ok { 10 | if bv, ok := out[k]; ok { 11 | if bv, ok := bv.(map[string]any); ok { 12 | out[k] = Merge(bv, v) 13 | continue 14 | } 15 | } 16 | } 17 | out[k] = v 18 | } 19 | return out 20 | } 21 | -------------------------------------------------------------------------------- /pkg/validation/config/configuration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateConfiguration(obj *v1alpha1.Configuration) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | var path *field.Path 12 | errs = append(errs, ValidateConfigurationSpec(path.Child("spec"), obj.Spec)...) 13 | } 14 | return errs 15 | } 16 | -------------------------------------------------------------------------------- /testdata/e2e/config-v1alpha2.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/configuration-chainsaw-v1alpha2.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha2 3 | kind: Configuration 4 | metadata: 5 | creationTimestamp: null 6 | name: configuration 7 | spec: 8 | discovery: 9 | fullName: true 10 | execution: 11 | failFast: true 12 | forceTerminationGracePeriod: 5s 13 | parallel: 1 14 | -------------------------------------------------------------------------------- /pkg/runner/clusters/register.go: -------------------------------------------------------------------------------- 1 | package clusters 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 7 | ) 8 | 9 | func Register(registry Registry, basePath string, clusters map[string]v1alpha1.Cluster) Registry { 10 | for name, cluster := range clusters { 11 | registry = registry.Register(name, NewClusterFromKubeconfig(filepath.Join(basePath, cluster.Kubeconfig), cluster.Context)) 12 | } 13 | return registry 14 | } 15 | -------------------------------------------------------------------------------- /pkg/runner/internal/flags.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "testing" 6 | 7 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 8 | "github.com/kyverno/chainsaw/pkg/runner/flags" 9 | ) 10 | 11 | func SetupFlags(config v1alpha1.ConfigurationSpec) error { 12 | testing.Init() 13 | for k, v := range flags.GetFlags(config) { 14 | if err := flag.Set(k, v); err != nil { 15 | return err 16 | } 17 | } 18 | flag.Parse() 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /testdata/e2e/examples/sleep/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: sleep 6 | spec: 7 | steps: 8 | - try: 9 | - sleep: 10 | duration: 10s 11 | catch: 12 | - sleep: 13 | duration: 5s 14 | finally: 15 | - sleep: 16 | duration: 5s 17 | -------------------------------------------------------------------------------- /pkg/runner/clusters/resolve.go: -------------------------------------------------------------------------------- 1 | package clusters 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/client" 5 | "k8s.io/client-go/rest" 6 | ) 7 | 8 | func link(cluster Cluster, dryRun bool) (*rest.Config, client.Client, error) { 9 | config, err := cluster.Config() 10 | if err != nil { 11 | return nil, nil, err 12 | } 13 | client, err := makeClient(config, dryRun) 14 | if err != nil { 15 | return nil, nil, err 16 | } 17 | return config, client, nil 18 | } 19 | -------------------------------------------------------------------------------- /testdata/e2e/examples/timeout/README.md: -------------------------------------------------------------------------------- 1 | # Test: `timeout` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 1 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `script` | 0 | 0 | *No description* | 20 | 21 | --- 22 | 23 | -------------------------------------------------------------------------------- /testdata/e2e/examples/values/README.md: -------------------------------------------------------------------------------- 1 | # Test: `values` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 1 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `assert` | 0 | 0 | *No description* | 20 | 21 | --- 22 | 23 | -------------------------------------------------------------------------------- /pkg/mutate/mutate.go: -------------------------------------------------------------------------------- 1 | package mutate 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmespath-community/go-jmespath/pkg/binding" 7 | "github.com/kyverno/kyverno-json/pkg/engine/template" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | func Mutate(ctx context.Context, path *field.Path, mutation Mutation, value any, bindings binding.Bindings, opts ...template.Option) (any, error) { 12 | return mutation.mutate(ctx, path, value, bindings, opts...) 13 | } 14 | -------------------------------------------------------------------------------- /testdata/e2e/examples/array-assertion/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: example 5 | spec: 6 | containers: 7 | - name: container-1 8 | image: nginx-1 9 | env: 10 | - name: ENV_1 11 | value: value-1 12 | - name: container-2 13 | image: nginx-2 14 | env: 15 | - name: ENV_2 16 | value: value-2 17 | - name: container-3 18 | image: nginx-3 19 | env: 20 | - name: ENV_3 21 | value: value-3 22 | -------------------------------------------------------------------------------- /testdata/kuttl/.chainsaw.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/configuration-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Configuration 4 | metadata: 5 | creationTimestamp: null 6 | name: configuration 7 | spec: 8 | parallel: 4 9 | timeouts: 10 | apply: 5m0s 11 | assert: 5m0s 12 | cleanup: 5m0s 13 | delete: 5m0s 14 | error: 5m0s 15 | exec: 5m0s 16 | -------------------------------------------------------------------------------- /testdata/commands/build/docs/help.txt: -------------------------------------------------------------------------------- 1 | Build tests documentation 2 | 3 | Usage: 4 | chainsaw docs [flags] 5 | 6 | Flags: 7 | --catalog string Path to the built test catalog file 8 | -h, --help help for docs 9 | --readme-file string Name of the built docs file (default "README.md") 10 | --test-dir stringArray Directories containing test cases to run 11 | --test-file string Name of the test file (default "chainsaw-test") 12 | -------------------------------------------------------------------------------- /website/docs/configuration/no-cluster.md: -------------------------------------------------------------------------------- 1 | # Running without a cluster 2 | 3 | Chainsaw can be run without connection to a Kubernetes cluster. 4 | In this case, chainsaw will not try to create an ephemeral namespace and all operations requiring a Kubernetes cluster will fail. 5 | 6 | To run chainsaw in this mode pass the `--no-cluster` flag. 7 | 8 | ## Example 9 | 10 | ```bash 11 | # run chainsaw without connection to a Kubernetes cluster 12 | chainsaw test --no-cluster 13 | ``` 14 | -------------------------------------------------------------------------------- /testdata/e2e/examples/assertion-tree/README.md: -------------------------------------------------------------------------------- 1 | # Test: `assertion-tree` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 1 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `assert` | 0 | 0 | *No description* | 20 | 21 | --- 22 | 23 | -------------------------------------------------------------------------------- /testdata/kuttl/01-step.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestStep 3 | delete: 4 | # Delete a Pod 5 | - apiVersion: v1 6 | kind: Pod 7 | name: my-pod 8 | # Delete all Pods with app=nginx 9 | - apiVersion: v1 10 | kind: Pod 11 | labels: 12 | app: nginx 13 | # Delete all Pods in the test namespace 14 | - apiVersion: v1 15 | kind: Pod 16 | commands: 17 | - script: echo "hello world" 18 | skipLogOutput: true 19 | - command: echo "hello world" 20 | skipLogOutput: true 21 | -------------------------------------------------------------------------------- /testdata/kuttl/02-step.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestStep 3 | delete: 4 | # Delete a Pod 5 | - apiVersion: v1 6 | kind: Pod 7 | name: my-pod 8 | # Delete all Pods with app=nginx 9 | - apiVersion: v1 10 | kind: Pod 11 | labels: 12 | app: nginx 13 | # Delete all Pods in the test namespace 14 | - apiVersion: v1 15 | kind: Pod 16 | commands: 17 | - script: echo "hello world" 18 | skipLogOutput: true 19 | - command: echo "hello world" 20 | skipLogOutput: true 21 | -------------------------------------------------------------------------------- /pkg/runner/clusters/client.go: -------------------------------------------------------------------------------- 1 | package clusters 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/client" 5 | runnerclient "github.com/kyverno/chainsaw/pkg/runner/client" 6 | "k8s.io/client-go/rest" 7 | ) 8 | 9 | func makeClient(config *rest.Config, dryRun bool) (client.Client, error) { 10 | c, err := client.New(config) 11 | if err != nil { 12 | return nil, err 13 | } 14 | c = runnerclient.New(c) 15 | if !dryRun { 16 | return c, nil 17 | } 18 | return client.DryRun(c), nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/runner/operations/testing/mock.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmespath-community/go-jmespath/pkg/binding" 7 | "github.com/kyverno/chainsaw/pkg/runner/operations" 8 | ) 9 | 10 | type MockOperation struct { 11 | ExecFn func(context.Context, binding.Bindings) (operations.Outputs, error) 12 | } 13 | 14 | func (m MockOperation) Exec(ctx context.Context, bindings binding.Bindings) (operations.Outputs, error) { 15 | return m.ExecFn(ctx, bindings) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/commands/migrate/kuttl/config/out.txt: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/configuration-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Configuration 4 | metadata: 5 | creationTimestamp: null 6 | name: configuration 7 | spec: 8 | parallel: 4 9 | timeouts: 10 | apply: 5m0s 11 | assert: 5m0s 12 | cleanup: 5m0s 13 | delete: 5m0s 14 | error: 5m0s 15 | exec: 5m0s 16 | 17 | -------------------------------------------------------------------------------- /testdata/e2e/examples/namespace-template/README.md: -------------------------------------------------------------------------------- 1 | # Test: `namespace-template` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 1 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `assert` | 0 | 0 | *No description* | 20 | 21 | --- 22 | 23 | -------------------------------------------------------------------------------- /.release-notes/v0.0.3.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.0.3`. 4 | 5 | ## 💫 New features 💫 6 | 7 | - We now have a [logo](https://github.com/kyverno/chainsaw/blob/main/.assets/kyverno-chainsaw-horizontal.png) :tada: 8 | - Timeouts are now managed per operation rather than per step 9 | - Added timeout support at the operation level 10 | 11 | ## 🔧 Fixes 🔧 12 | 13 | - Fixed `Exec` collectors not invoked 14 | 15 | ## 🎸 Misc 🎸 16 | 17 | - Improved error messages in a couple of places 18 | -------------------------------------------------------------------------------- /pkg/validation/test/assert.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateAssert(path *field.Path, obj *v1alpha1.Assert) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateFileRefOrCheck(path, obj.FileRefOrCheck)...) 12 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 13 | } 14 | return errs 15 | } 16 | -------------------------------------------------------------------------------- /pkg/validation/test/error.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateError(path *field.Path, obj *v1alpha1.Error) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateFileRefOrCheck(path, obj.FileRefOrCheck)...) 12 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 13 | } 14 | return errs 15 | } 16 | -------------------------------------------------------------------------------- /pkg/validation/test/events.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateEvents(path *field.Path, obj *v1alpha1.Events) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Name != "" && obj.Selector != "" { 12 | errs = append(errs, field.Invalid(path, obj, "a name or label selector must be specified (found both)")) 13 | } 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/runner/internal/corpus_entry.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // corpusEntry is from the public go testing which references an internal structure. 4 | // corpusEntry is an alias to the same type as internal/fuzz.CorpusEntry. 5 | // We use a type alias because we don't want to export this type, and we can't 6 | // import internal/fuzz from testing. 7 | type corpusEntry = struct { 8 | Parent string 9 | Path string 10 | Data []byte 11 | Values []any 12 | Generation int 13 | IsSeed bool 14 | } 15 | -------------------------------------------------------------------------------- /testdata/e2e/examples/delete/README.md: -------------------------------------------------------------------------------- 1 | # Test: `delete` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `delete` | 0 | 0 | *No description* | 21 | 22 | --- 23 | 24 | -------------------------------------------------------------------------------- /testdata/e2e/examples/inline/README.md: -------------------------------------------------------------------------------- 1 | # Test: `inline` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | 22 | --- 23 | 24 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_migrate_kuttl_tests.md: -------------------------------------------------------------------------------- 1 | ## chainsaw migrate kuttl tests 2 | 3 | Migrate KUTTL tests to Chainsaw 4 | 5 | ``` 6 | chainsaw migrate kuttl tests [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --cleanup If set, delete converted files 13 | -h, --help help for tests 14 | --save If set, converted files will be saved 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [chainsaw migrate kuttl](chainsaw_migrate_kuttl.md) - Migrate KUTTL resources to Chainsaw 20 | 21 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/object_reference.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // ObjectReference represents one or more objects with a specific apiVersion and kind. 4 | // For a single object name and namespace are used to identify the object. 5 | // For multiple objects use labels. 6 | type ObjectReference struct { 7 | // ObjectType determines the type of referenced objects. 8 | ObjectType `json:",inline"` 9 | 10 | // ObjectSelector determines the selection process of referenced objects. 11 | ObjectSelector `json:",inline"` 12 | } 13 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_migrate_kuttl_config.md: -------------------------------------------------------------------------------- 1 | ## chainsaw migrate kuttl config 2 | 3 | Migrate KUTTL config to Chainsaw 4 | 5 | ``` 6 | chainsaw migrate kuttl config [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --cleanup If set, delete converted files 13 | -h, --help help for config 14 | --save If set, converted files will be saved 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [chainsaw migrate kuttl](chainsaw_migrate_kuttl.md) - Migrate KUTTL resources to Chainsaw 20 | 21 | -------------------------------------------------------------------------------- /pkg/internal/loader/testing/fake_loader.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | ) 7 | 8 | type FakeLoader struct { 9 | LoadFn func(int, []byte) (schema.GroupVersionKind, unstructured.Unstructured, error) 10 | numCalls int 11 | } 12 | 13 | func (f *FakeLoader) Load(data []byte) (schema.GroupVersionKind, unstructured.Unstructured, error) { 14 | defer func() { f.numCalls++ }() 15 | return f.LoadFn(f.numCalls, data) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/e2e/examples/deployment/README.md: -------------------------------------------------------------------------------- 1 | # Test: `deployment` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | 22 | --- 23 | 24 | -------------------------------------------------------------------------------- /pkg/client/namespace.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | func Namespace(name string) corev1.Namespace { 9 | return corev1.Namespace{ 10 | TypeMeta: metav1.TypeMeta{ 11 | APIVersion: corev1.SchemeGroupVersion.String(), 12 | Kind: "Namespace", 13 | }, 14 | ObjectMeta: metav1.ObjectMeta{ 15 | Name: name, 16 | }, 17 | } 18 | } 19 | 20 | func PetNamespace() corev1.Namespace { 21 | return Namespace(Pet()) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/commands/build/command.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/commands/build/docs" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func Command() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "build", 11 | Short: "Build commands", 12 | Args: cobra.NoArgs, 13 | SilenceUsage: true, 14 | RunE: func(cmd *cobra.Command, _ []string) error { 15 | return cmd.Help() 16 | }, 17 | } 18 | cmd.AddCommand( 19 | docs.Command(), 20 | ) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_docs.md: -------------------------------------------------------------------------------- 1 | ## chainsaw docs 2 | 3 | Generate reference documentation 4 | 5 | ``` 6 | chainsaw docs [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --autogenTag Determines if the generated docs should contain a timestamp (default true) 13 | -h, --help help for docs 14 | -o, --output string Output path (default ".") 15 | --website Website version 16 | ``` 17 | 18 | ### SEE ALSO 19 | 20 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 21 | 22 | -------------------------------------------------------------------------------- /pkg/runner/env/expand.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func Expand(env map[string]string, in ...string) []string { 8 | var args []string 9 | for _, arg := range in { 10 | expanded := os.Expand(arg, func(key string) string { 11 | expanded := env[key] 12 | if expanded == "" { 13 | expanded = os.Getenv(key) 14 | } 15 | return expanded 16 | }) 17 | if expanded != "" { 18 | args = append(args, expanded) 19 | } else { 20 | args = append(args, arg) 21 | } 22 | } 23 | return args 24 | } 25 | -------------------------------------------------------------------------------- /testdata/e2e/examples/array-assertion/README.md: -------------------------------------------------------------------------------- 1 | # Test: `array-assertions` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | 22 | --- 23 | 24 | -------------------------------------------------------------------------------- /pkg/commands/export/command.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/commands/export/schemas" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func Command() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "export", 11 | Short: "Export commands", 12 | Args: cobra.NoArgs, 13 | SilenceUsage: true, 14 | RunE: func(cmd *cobra.Command, _ []string) error { 15 | return cmd.Help() 16 | }, 17 | } 18 | cmd.AddCommand( 19 | schemas.Command(), 20 | ) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /testdata/e2e/examples/command-output/README.md: -------------------------------------------------------------------------------- 1 | # Test: `command-output` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [Check bad kubectl command](#step-Check bad kubectl command) | 0 | 1 | 0 | 0 | 10 | 11 | ### Step: `Check bad kubectl command` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `script` | 0 | 0 | *No description* | 20 | 21 | --- 22 | 23 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/wait_for.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // For specifies the condition to wait for. 4 | type For struct { 5 | // Deletion specifies parameters for waiting on a resource's deletion. 6 | // +optional 7 | Deletion *Deletion `json:"deletion,omitempty"` 8 | 9 | // Condition specifies the condition to wait for. 10 | // +optional 11 | Condition *Condition `json:"condition,omitempty"` 12 | 13 | // JsonPath specifies the json path condition to wait for. 14 | // +optional 15 | JsonPath *JsonPath `json:"jsonPath,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/commands/create/command.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/commands/create/test" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func Command() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "create", 11 | Short: "Create Chainsaw resources", 12 | Args: cobra.NoArgs, 13 | SilenceUsage: true, 14 | RunE: func(cmd *cobra.Command, _ []string) error { 15 | return cmd.Help() 16 | }, 17 | } 18 | cmd.AddCommand( 19 | test.Command(), 20 | ) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /pkg/internal/loader/default.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "io/fs" 5 | 6 | "github.com/kyverno/chainsaw/pkg/data" 7 | "github.com/kyverno/pkg/ext/resource/loader" 8 | "k8s.io/client-go/openapi" 9 | "sigs.k8s.io/kubectl-validate/pkg/openapiclient" 10 | ) 11 | 12 | var ( 13 | OpenApiClient = func() openapi.Client { 14 | fs, err := fs.Sub(data.Crds(), data.CrdsFolder) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return openapiclient.NewLocalCRDFiles(fs) 19 | }() 20 | DefaultLoader, Err = loader.New(OpenApiClient) 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/commands/migrate/command.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/commands/migrate/kuttl" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func Command() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "migrate", 11 | Short: "Migrate resources to Chainsaw", 12 | Args: cobra.NoArgs, 13 | SilenceUsage: true, 14 | RunE: func(cmd *cobra.Command, _ []string) error { 15 | return cmd.Help() 16 | }, 17 | } 18 | cmd.AddCommand( 19 | kuttl.Command(), 20 | ) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /testdata/e2e/examples/list/README.md: -------------------------------------------------------------------------------- 1 | # Test: `list` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 3 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | | 3 | `assert` | 0 | 0 | *No description* | 22 | 23 | --- 24 | 25 | -------------------------------------------------------------------------------- /testdata/e2e/examples/patch/README.md: -------------------------------------------------------------------------------- 1 | # Test: `patch` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 3 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `patch` | 0 | 0 | *No description* | 21 | | 3 | `assert` | 0 | 0 | *No description* | 22 | 23 | --- 24 | 25 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_create_test.md: -------------------------------------------------------------------------------- 1 | ## chainsaw create test 2 | 3 | Create a Chainsaw test 4 | 5 | ``` 6 | chainsaw create test [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --description If set, adds description when applicable (default true) 13 | --force If set, existing test will be deleted if needed 14 | -h, --help help for test 15 | --save If set, created test will be saved 16 | ``` 17 | 18 | ### SEE ALSO 19 | 20 | * [chainsaw create](chainsaw_create.md) - Create Chainsaw resources 21 | 22 | -------------------------------------------------------------------------------- /pkg/client/namespace_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | corev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | func TestNamespace(t *testing.T) { 11 | expectedName := "testNamespace" 12 | ns := Namespace(expectedName) 13 | 14 | assert.Equal(t, corev1.SchemeGroupVersion.String(), ns.APIVersion) 15 | assert.Equal(t, "Namespace", ns.Kind) 16 | assert.Equal(t, expectedName, ns.Name) 17 | } 18 | 19 | func TestPetNamespace(t *testing.T) { 20 | ns := PetNamespace() 21 | 22 | assert.NotEmpty(t, ns.Name) 23 | } 24 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_lint.md: -------------------------------------------------------------------------------- 1 | ## chainsaw lint 2 | 3 | Lint a file or read from standard input 4 | 5 | ### Synopsis 6 | 7 | Use chainsaw lint to lint a specific file or read from standard input for either test or configuration. 8 | 9 | ``` 10 | chainsaw lint [test|configuration] [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -f, --file string Specify the file to lint or '-' for standard input 17 | -h, --help help for lint 18 | ``` 19 | 20 | ### SEE ALSO 21 | 22 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 23 | 24 | -------------------------------------------------------------------------------- /pkg/runner/functions/caller.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "context" 5 | 6 | jpfunctions "github.com/jmespath-community/go-jmespath/pkg/functions" 7 | "github.com/jmespath-community/go-jmespath/pkg/interpreter" 8 | "github.com/kyverno/kyverno-json/pkg/engine/template" 9 | ) 10 | 11 | var Caller = func() interpreter.FunctionCaller { 12 | var funcs []jpfunctions.FunctionEntry 13 | funcs = append(funcs, template.GetFunctions(context.Background())...) 14 | funcs = append(funcs, GetFunctions()...) 15 | return interpreter.NewFunctionCaller(funcs...) 16 | }() 17 | -------------------------------------------------------------------------------- /testdata/commands/lint/configuration/configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: valid 5 | namespace: kyverno 6 | spec: 7 | timeouts: 8 | apply: 5s 9 | assert: 10s 10 | error: 10s 11 | delete: 5s 12 | cleanup: 5s 13 | exec: 10s 14 | skipDelete: true 15 | failFast: true 16 | parallel: 5 17 | reportFormat: JSON 18 | reportName: custom-chainsaw-report 19 | namespace: test-namespace 20 | fullName: true 21 | includeTestRegex: ^include-.* 22 | excludeTestRegex: ^exclude-.* 23 | -------------------------------------------------------------------------------- /testdata/commands/test/config/config_all_fields.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: valid 5 | namespace: kyverno 6 | spec: 7 | timeouts: 8 | apply: 5s 9 | assert: 10s 10 | error: 10s 11 | delete: 5s 12 | cleanup: 5s 13 | exec: 10s 14 | skipDelete: true 15 | failFast: true 16 | parallel: 5 17 | reportFormat: JSON 18 | reportName: custom-chainsaw-report 19 | namespace: test-namespace 20 | fullName: true 21 | includeTestRegex: ^include-.* 22 | excludeTestRegex: ^exclude-.* 23 | -------------------------------------------------------------------------------- /testdata/config/custom-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: custom-config 5 | spec: 6 | timeouts: 7 | apply: 5s 8 | assert: 10s 9 | error: 10s 10 | delete: 5s 11 | cleanup: 5s 12 | exec: 10s 13 | skipDelete: true 14 | testFile: custom-test.yaml 15 | failFast: true 16 | parallel: 4 17 | reportFormat: "JSON" 18 | reportName: "custom-report" 19 | fullName: true 20 | includeTestRegex: "include-*" 21 | excludeTestRegex: "exclude-*" 22 | forceTerminationGracePeriod: 10s 23 | -------------------------------------------------------------------------------- /.release-notes/v0.1.0.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.1.0`. 4 | 5 | ## 💫 New features 💫 6 | 7 | - Chainsaw is now available via brew 8 | - Added support for defining multiple tests in the same test file 9 | 10 | ## 🔧 Fixes 🔧 11 | 12 | - Fixed an incorrect error message when loading a resource failed 13 | 14 | ## ✨ UI changes ✨ 15 | 16 | - Show Chainsaw version in `chainsaw test` command output 17 | 18 | ## 📚 Docs 📚 19 | 20 | - Added docs about parallel execution of Tests 21 | 22 | ## 🎸 Misc 🎸 23 | 24 | - Added an ADOPTERS.md file to the repository 25 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_migrate_kuttl.md: -------------------------------------------------------------------------------- 1 | ## chainsaw migrate kuttl 2 | 3 | Migrate KUTTL resources to Chainsaw 4 | 5 | ``` 6 | chainsaw migrate kuttl [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for kuttl 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw migrate](chainsaw_migrate.md) - Migrate resources to Chainsaw 18 | * [chainsaw migrate kuttl config](chainsaw_migrate_kuttl_config.md) - Migrate KUTTL config to Chainsaw 19 | * [chainsaw migrate kuttl tests](chainsaw_migrate_kuttl_tests.md) - Migrate KUTTL tests to Chainsaw 20 | 21 | -------------------------------------------------------------------------------- /pkg/runner/functions/utils.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func stable(in string) string { 9 | return in 10 | } 11 | 12 | func experimental(in string) string { 13 | return "x_" + in 14 | } 15 | 16 | func getArg[T any](arguments []any, index int, out *T) error { 17 | if index >= len(arguments) { 18 | return fmt.Errorf("index out of range (%d / %d)", index, len(arguments)) 19 | } 20 | if value, ok := arguments[index].(T); !ok { 21 | return errors.New("invalid type") 22 | } else { 23 | *out = value 24 | return nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/validation/test/get.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateGet(path *field.Path, obj *v1alpha1.Get) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Name != "" && obj.Selector != "" { 12 | errs = append(errs, field.Invalid(path, obj, "a name or label selector must be specified (found both)")) 13 | } 14 | errs = append(errs, ValidateResourceReference(path, obj.ResourceReference)...) 15 | } 16 | return errs 17 | } 18 | -------------------------------------------------------------------------------- /testdata/commands/lint/configuration/wrong-configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha2 2 | kind: Configurations 3 | metadata: 4 | name: valid 5 | namespace: kyverno 6 | spec: 7 | timeouts: 8 | apply: 5s 9 | assert: 10s 10 | error: 10s 11 | delete: 5s 12 | cleanup: 5s 13 | exec: 10s 14 | skipDelete: true 15 | failFast: true 16 | parallel: 5 17 | reportFormat: JSON 18 | reportName: custom-chainsaw-report 19 | namespace: test-namespace 20 | fullName: true 21 | includeTestRegex: ^include-.* 22 | excludeTestRegex: ^exclude-.* 23 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 9 | // +kubebuilder:object:root=true 10 | // +kubebuilder:resource:scope=Cluster 11 | 12 | // Test is the resource that contains a test definition. 13 | type Test struct { 14 | metav1.TypeMeta `json:",inline"` 15 | 16 | // Standard object's metadata. 17 | // +optional 18 | metav1.ObjectMeta `json:"metadata,omitempty"` 19 | 20 | // Test spec. 21 | Spec TestSpec `json:"spec"` 22 | } 23 | -------------------------------------------------------------------------------- /pkg/validation/test/apply.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateApply(path *field.Path, obj *v1alpha1.Apply) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateFileRefOrResource(path, obj.FileRefOrResource)...) 12 | errs = append(errs, ValidateExpectations(path.Child("expect"), obj.Expect...)...) 13 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/validation/test/create.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateCreate(path *field.Path, obj *v1alpha1.Create) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateFileRefOrResource(path, obj.FileRefOrResource)...) 12 | errs = append(errs, ValidateExpectations(path.Child("expect"), obj.Expect...)...) 13 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/validation/test/describe.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateDescribe(path *field.Path, obj *v1alpha1.Describe) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Name != "" && obj.Selector != "" { 12 | errs = append(errs, field.Invalid(path, obj, "a name or label selector must be specified (found both)")) 13 | } 14 | errs = append(errs, ValidateResourceReference(path, obj.ResourceReference)...) 15 | } 16 | return errs 17 | } 18 | -------------------------------------------------------------------------------- /pkg/validation/test/patch.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidatePatch(path *field.Path, obj *v1alpha1.Patch) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateFileRefOrResource(path, obj.FileRefOrResource)...) 12 | errs = append(errs, ValidateExpectations(path.Child("expect"), obj.Expect...)...) 13 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/validation/test/update.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateUpdate(path *field.Path, obj *v1alpha1.Update) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateFileRefOrResource(path, obj.FileRefOrResource)...) 12 | errs = append(errs, ValidateExpectations(path.Child("expect"), obj.Expect...)...) 13 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /testdata/test/raw-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test-1 5 | spec: 6 | steps: 7 | - try: 8 | - apply: 9 | resource: 10 | apiVersion: v1 11 | kind: ConfigMap 12 | metadata: 13 | name: chainsaw-quick-start 14 | data: 15 | foo: bar 16 | - try: 17 | - create: 18 | resource: 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: chainsaw-quick-start 23 | data: 24 | foo: bar 25 | -------------------------------------------------------------------------------- /pkg/validation/test/object_reference.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateObjectReference(path *field.Path, obj v1alpha1.ObjectReference) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj.Kind == "" { 11 | errs = append(errs, field.Invalid(path.Child("kind"), obj, "kind must be specified")) 12 | } 13 | if obj.APIVersion == "" { 14 | errs = append(errs, field.Invalid(path.Child("apiVersion"), obj, "apiVersion must be specified")) 15 | } 16 | return errs 17 | } 18 | -------------------------------------------------------------------------------- /testdata/e2e/examples/k8s-server-version/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: k8s-server-version 6 | spec: 7 | bindings: 8 | - name: version 9 | value: (x_k8s_server_version($config)) 10 | steps: 11 | - try: 12 | - script: 13 | env: 14 | - name: K8S_VERSION 15 | value: (to_string($version)) 16 | content: | 17 | set -e 18 | echo $K8S_VERSION 19 | -------------------------------------------------------------------------------- /pkg/validation/test/delete.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateDelete(path *field.Path, obj *v1alpha1.Delete) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | errs = append(errs, ValidateObjectReference(path.Child("ref"), obj.ObjectReference)...) 12 | errs = append(errs, ValidateExpectations(path.Child("expect"), obj.Expect...)...) 13 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/validation/test/expectations.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateExpectations(path *field.Path, expectations ...v1alpha1.Expectation) field.ErrorList { 9 | var errs field.ErrorList 10 | for i := range expectations { 11 | path := path.Index(i) 12 | expectation := expectations[i] 13 | errs = append(errs, ValidateCheck(path.Child("match"), expectation.Match)...) 14 | errs = append(errs, ValidateCheck(path.Child("check"), &expectation.Check)...) 15 | } 16 | return errs 17 | } 18 | -------------------------------------------------------------------------------- /testdata/e2e/examples/update/README.md: -------------------------------------------------------------------------------- 1 | # Test: `update` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 4 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `update` | 0 | 0 | *No description* | 21 | | 3 | `assert` | 0 | 0 | *No description* | 22 | | 4 | `error` | 0 | 0 | *No description* | 23 | 24 | --- 25 | 26 | -------------------------------------------------------------------------------- /pkg/runner/namespacer/testing/fake_namespacer.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 5 | ) 6 | 7 | type FakeNamespacer struct { 8 | ApplyFn func(obj ctrlclient.Object, call int) error 9 | GetNamespaceFn func(call int) string 10 | numCalls int 11 | } 12 | 13 | func (n *FakeNamespacer) Apply(obj ctrlclient.Object) error { 14 | defer func() { n.numCalls++ }() 15 | return n.ApplyFn(obj, n.numCalls) 16 | } 17 | 18 | func (n *FakeNamespacer) GetNamespace() string { 19 | defer func() { n.numCalls++ }() 20 | return n.GetNamespaceFn(n.numCalls) 21 | } 22 | -------------------------------------------------------------------------------- /testdata/e2e/examples/array-assertion/assertions.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: example 5 | spec: 6 | # iterate over all containers having `name: container-1` 7 | ~.(containers[?name == 'container-1']): 8 | image: nginx-1 9 | # iterate over all containers, bind `$index` to the element index 10 | ~index.(containers): 11 | image: (join('-', ['nginx', to_string($index + `1`)])) 12 | # nested iteration 13 | ~index2.(containers): 14 | ~.(env): 15 | name: (join('_', ['ENV', to_string($index2 + `1`)])) 16 | value: (join('-', ['value', to_string($index2 + `1`)])) 17 | -------------------------------------------------------------------------------- /pkg/validation/test/output.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateOutput(path *field.Path, obj v1alpha1.Output) field.ErrorList { 9 | var errs field.ErrorList 10 | errs = append(errs, ValidateBinding(path, obj.Binding)...) 11 | return errs 12 | } 13 | 14 | func ValidateOutputs(path *field.Path, objs ...v1alpha1.Output) field.ErrorList { 15 | var errs field.ErrorList 16 | for i, obj := range objs { 17 | errs = append(errs, ValidateOutput(path.Index(i), obj)...) 18 | } 19 | return errs 20 | } 21 | -------------------------------------------------------------------------------- /testdata/e2e/examples/k8s-server-version/README.md: -------------------------------------------------------------------------------- 1 | # Test: `k8s-server-version` 2 | 3 | *No description* 4 | 5 | ## Bindings 6 | 7 | | # | Name | Value | 8 | |:-:|---|---| 9 | | 1 | `version` | "(x_k8s_server_version($config))" | 10 | 11 | ## Steps 12 | 13 | | # | Name | Bindings | Try | Catch | Finally | 14 | |:-:|---|:-:|:-:|:-:|:-:| 15 | | 1 | [step-1](#step-step-1) | 0 | 1 | 0 | 0 | 16 | 17 | ### Step: `step-1` 18 | 19 | *No description* 20 | 21 | #### Try 22 | 23 | | # | Operation | Bindings | Outputs | Description | 24 | |:-:|---|:-:|:-:|---| 25 | | 1 | `script` | 0 | 0 | *No description* | 26 | 27 | --- 28 | 29 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/file_ref_or_resource.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | ) 6 | 7 | // FileRefOrResource represents a file reference or resource. 8 | type FileRefOrResource struct { 9 | // FileRef provides a reference to the file containing the resources to be applied. 10 | // +optional 11 | FileRef `json:",inline"` 12 | 13 | // Resource provides a resource to be applied. 14 | // +kubebuilder:validation:XEmbeddedResource 15 | // +kubebuilder:pruning:PreserveUnknownFields 16 | // +optional 17 | Resource *unstructured.Unstructured `json:"resource,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /testdata/e2e/examples/delete/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: delete 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | resource: 11 | apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: quick-start 15 | data: 16 | foo: bar 17 | - delete: 18 | ref: 19 | apiVersion: v1 20 | kind: ConfigMap 21 | name: quick-start 22 | -------------------------------------------------------------------------------- /.hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json 2 | 3 | blank_issues_enabled: true 4 | 5 | contact_links: 6 | - name: Chainsaw discussions 7 | url: https://github.com/kyverno/chainsaw/discussions 8 | about: Please ask and answer questions related to chainsaw here. 9 | - name: Chainsaw documentation 10 | url: https://kyverno.github.io/chainsaw 11 | about: Browse chainsaw documentation here. 12 | - name: Chainsaw slack channel 13 | url: https://kubernetes.slack.com/archives/C067LUFL43U 14 | about: Please ask and answer any chainsaw usage questions here. 15 | -------------------------------------------------------------------------------- /pkg/commands/migrate/kuttl/command.go: -------------------------------------------------------------------------------- 1 | package kuttl 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/commands/migrate/kuttl/config" 5 | "github.com/kyverno/chainsaw/pkg/commands/migrate/kuttl/tests" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func Command() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "kuttl", 12 | Short: "Migrate KUTTL resources to Chainsaw", 13 | Args: cobra.NoArgs, 14 | SilenceUsage: true, 15 | RunE: func(cmd *cobra.Command, _ []string) error { 16 | return cmd.Help() 17 | }, 18 | } 19 | cmd.AddCommand( 20 | config.Command(), 21 | tests.Command(), 22 | ) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /pkg/validation/test/file_ref_or_check.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateFileRefOrCheck(path *field.Path, obj v1alpha1.FileRefOrCheck) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj.File == "" && obj.Check == nil { 11 | errs = append(errs, field.Invalid(path, obj, "a file reference or raw check must be specified")) 12 | } else if obj.File != "" && obj.Check != nil { 13 | errs = append(errs, field.Invalid(path, obj, "a file reference or raw check must be specified (found both)")) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /pkg/validation/test/wait.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateWait(path *field.Path, obj *v1alpha1.Wait) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Name != "" && obj.Selector != "" { 12 | errs = append(errs, field.Invalid(path, obj, "a name or label selector must be specified (found both)")) 13 | } 14 | errs = append(errs, ValidateResourceReference(path, obj.ResourceReference)...) 15 | errs = append(errs, ValidateFor(path.Child("for"), &obj.For)...) 16 | } 17 | return errs 18 | } 19 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_build_docs.md: -------------------------------------------------------------------------------- 1 | ## chainsaw build docs 2 | 3 | Build tests documentation 4 | 5 | ``` 6 | chainsaw build docs [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --catalog string Path to the built test catalog file 13 | -h, --help help for docs 14 | --readme-file string Name of the built docs file (default "README.md") 15 | --test-dir stringArray Directories containing test cases to run 16 | --test-file string Name of the test file (default "chainsaw-test") 17 | ``` 18 | 19 | ### SEE ALSO 20 | 21 | * [chainsaw build](chainsaw_build.md) - Build commands 22 | 23 | -------------------------------------------------------------------------------- /pkg/runner/summary/summary.go: -------------------------------------------------------------------------------- 1 | package summary 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | type Summary struct { 8 | passed atomic.Int32 9 | failed atomic.Int32 10 | skipped atomic.Int32 11 | } 12 | 13 | func (s *Summary) IncPassed() { 14 | s.passed.Add(1) 15 | } 16 | 17 | func (s *Summary) IncFailed() { 18 | s.failed.Add(1) 19 | } 20 | 21 | func (s *Summary) IncSkipped() { 22 | s.skipped.Add(1) 23 | } 24 | 25 | func (s *Summary) Passed() int32 { 26 | return s.passed.Load() 27 | } 28 | 29 | func (s *Summary) Failed() int32 { 30 | return s.failed.Load() 31 | } 32 | 33 | func (s *Summary) Skipped() int32 { 34 | return s.skipped.Load() 35 | } 36 | -------------------------------------------------------------------------------- /pkg/validation/test/pod_logs.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidatePodLogs(path *field.Path, obj *v1alpha1.PodLogs) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Name == "" && obj.Selector == "" { 12 | errs = append(errs, field.Invalid(path, obj, "name or label selector must be specified")) 13 | } 14 | if obj.Name != "" && obj.Selector != "" { 15 | errs = append(errs, field.Invalid(path, obj, "a name or label selector must be specified (found both)")) 16 | } 17 | } 18 | return errs 19 | } 20 | -------------------------------------------------------------------------------- /pkg/runner/mutate/convert_test.go: -------------------------------------------------------------------------------- 1 | package mutate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_convert(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | in any 13 | out map[string]any 14 | }{{ 15 | name: "nil", 16 | }, { 17 | name: "int", 18 | in: 42, 19 | out: nil, 20 | }, { 21 | name: "ok", 22 | in: map[any]any{ 23 | "foo": "bar", 24 | }, 25 | out: map[string]any{ 26 | "foo": "bar", 27 | }, 28 | }} 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | got := convertMap(tt.in) 32 | assert.Equal(t, tt.out, got) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/validation/test/file_ref_or_resource.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateFileRefOrResource(path *field.Path, obj v1alpha1.FileRefOrResource) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj.File == "" && obj.Resource == nil { 11 | errs = append(errs, field.Invalid(path, obj, "a file reference or raw resource must be specified")) 12 | } else if obj.File != "" && obj.Resource != nil { 13 | errs = append(errs, field.Invalid(path, obj, "a file reference or raw resource must be specified (found both)")) 14 | } 15 | return errs 16 | } 17 | -------------------------------------------------------------------------------- /testdata/discovery/multiple-tests/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: test-1 5 | spec: 6 | steps: 7 | - name: create configmap 8 | try: 9 | - apply: 10 | file: configmap.yaml 11 | - name: assert configmap 12 | try: 13 | - assert: 14 | file: configmap.yaml 15 | --- 16 | apiVersion: chainsaw.kyverno.io/v1alpha1 17 | kind: Test 18 | metadata: 19 | name: test-2 20 | spec: 21 | steps: 22 | - name: create configmap 23 | try: 24 | - apply: 25 | file: configmap.yaml 26 | - name: assert configmap 27 | try: 28 | - assert: 29 | file: configmap.yaml 30 | -------------------------------------------------------------------------------- /testdata/e2e/examples/orphan/README.md: -------------------------------------------------------------------------------- 1 | # Test: `delete-test` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [delete-test](#step-delete-test) | 0 | 5 | 0 | 0 | 10 | 11 | ### Step: `delete-test` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `sleep` | 0 | 0 | *No description* | 21 | | 3 | `delete` | 0 | 0 | *No description* | 22 | | 4 | `sleep` | 0 | 0 | *No description* | 23 | | 5 | `error` | 0 | 0 | *No description* | 24 | 25 | --- 26 | 27 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/resource_reference.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // ResourceReference represents a resource (API), it can be represented with a resource or a kind. 4 | // Optionally an apiVersion can be specified. 5 | type ResourceReference struct { 6 | // API version of the referent. 7 | // +optional 8 | APIVersion string `json:"apiVersion,omitempty"` 9 | 10 | // Kind of the referent. 11 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 12 | // +optional 13 | Kind string `json:"kind,omitempty"` 14 | 15 | // Resource name of the referent. 16 | // +optional 17 | Resource string `json:"resource,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /testdata/e2e/examples/command-output/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: command-output 6 | spec: 7 | steps: 8 | - name: Check bad kubectl command 9 | try: 10 | - script: 11 | content: kubectl foo 12 | check: 13 | # This checks that the result of the content was an error. 14 | ($error != null): true 15 | # This check below ensures that the string 'top' is found in stderr or else fails 16 | (contains($stderr, 'top')): true 17 | -------------------------------------------------------------------------------- /.release-notes/v0.1.2.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.1.2`. 4 | 5 | ## 💫 New features 💫 6 | 7 | - Added `chainsaw lint` command to lint tests 8 | - Added non-resource assertions support 9 | - Added experimental JMESPath functions prefix (`x_`) 10 | - Added JMESPath function to interact with the cluster under test 11 | 12 | ## 🔧 Fixes 🔧 13 | 14 | - Retry not working properly in `create` and `apply` operations 15 | 16 | ## 📚 Docs 📚 17 | 18 | - Added _Examples_ docs section 19 | - Added community meeting docs 20 | 21 | ## 🎸 Misc 🎸 22 | 23 | - Added pull request template 24 | - Added issue templates 25 | - GitHub action was removed from the main chainsaw repository 26 | -------------------------------------------------------------------------------- /testdata/commands/test/default.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: chainsaw-test 4 | - TestDirs [.] 5 | - SkipDelete false 6 | - FailFast false 7 | - ReportFormat '' 8 | - ReportName 'chainsaw-report' 9 | - Namespace '' 10 | - FullName false 11 | - IncludeTestRegex '' 12 | - ExcludeTestRegex '' 13 | - ApplyTimeout 5s 14 | - AssertTimeout 30s 15 | - CleanupTimeout 30s 16 | - DeleteTimeout 15s 17 | - ErrorTimeout 30s 18 | - ExecTimeout 5s 19 | - NoCluster false 20 | - PauseOnFailure false 21 | Loading tests... 22 | Loading values... 23 | Running tests... 24 | Tests Summary... 25 | - Passed tests 0 26 | - Failed tests 0 27 | - Skipped tests 0 28 | Done. 29 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/configuration.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 9 | // +kubebuilder:object:root=true 10 | // +kubebuilder:resource:scope=Cluster 11 | // +kubebuilder:storageversion 12 | 13 | // Configuration is the resource that contains the configuration used to run tests. 14 | type Configuration struct { 15 | metav1.TypeMeta `json:",inline"` 16 | 17 | // Standard object's metadata. 18 | // +optional 19 | metav1.ObjectMeta `json:"metadata,omitempty"` 20 | 21 | // Configuration spec. 22 | Spec ConfigurationSpec `json:"spec"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/commands/version/command.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kyverno/chainsaw/pkg/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func Command() *cobra.Command { 11 | return &cobra.Command{ 12 | Use: "version", 13 | Short: "Print the version informations", 14 | Args: cobra.NoArgs, 15 | SilenceUsage: true, 16 | RunE: func(cmd *cobra.Command, _ []string) error { 17 | fmt.Fprintf(cmd.OutOrStdout(), "Version: %s\n", version.Version()) 18 | fmt.Fprintf(cmd.OutOrStdout(), "Time: %s\n", version.Time()) 19 | fmt.Fprintf(cmd.OutOrStdout(), "Git commit ID: %s\n", version.Hash()) 20 | return nil 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/validation/config/configuration_spec.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "github.com/kyverno/chainsaw/pkg/validation/test" 6 | "k8s.io/apimachinery/pkg/util/validation/field" 7 | ) 8 | 9 | func ValidateConfigurationSpec(path *field.Path, obj v1alpha1.ConfigurationSpec) field.ErrorList { 10 | var errs field.ErrorList 11 | path = path.Child("clusters") 12 | for name, cluster := range obj.Clusters { 13 | errs = append(errs, ValidateCluster(path.Key(name), cluster)...) 14 | } 15 | for i, catch := range obj.Catch { 16 | errs = append(errs, test.ValidateCatch(path.Child("catch").Index(i), catch)...) 17 | } 18 | return errs 19 | } 20 | -------------------------------------------------------------------------------- /testdata/commands/test/with_timeout.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: chainsaw-test 4 | - TestDirs [.] 5 | - SkipDelete false 6 | - FailFast false 7 | - ReportFormat '' 8 | - ReportName 'chainsaw-report' 9 | - Namespace '' 10 | - FullName false 11 | - IncludeTestRegex '' 12 | - ExcludeTestRegex '' 13 | - ApplyTimeout 10s 14 | - AssertTimeout 30s 15 | - CleanupTimeout 30s 16 | - DeleteTimeout 15s 17 | - ErrorTimeout 30s 18 | - ExecTimeout 5s 19 | - NoCluster false 20 | - PauseOnFailure false 21 | Loading tests... 22 | Loading values... 23 | Running tests... 24 | Tests Summary... 25 | - Passed tests 0 26 | - Failed tests 0 27 | - Skipped tests 0 28 | Done. 29 | -------------------------------------------------------------------------------- /testdata/e2e/examples/namespace-template/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: namespace-template 6 | spec: 7 | namespaceTemplate: 8 | metadata: 9 | name: foo 10 | annotations: 11 | keptn.sh/lifecycle-toolkit: enabled 12 | steps: 13 | - try: 14 | - assert: 15 | resource: 16 | apiVersion: v1 17 | kind: Namespace 18 | metadata: 19 | name: foo 20 | annotations: 21 | keptn.sh/lifecycle-toolkit: enabled 22 | -------------------------------------------------------------------------------- /testdata/e2e/examples/non-resource-assertion/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: non-resource-assertion 6 | spec: 7 | steps: 8 | - try: 9 | - assert: 10 | file: assert.yaml 11 | - error: 12 | file: error.yaml 13 | - try: 14 | - assert: 15 | resource: 16 | (x_k8s_list($client, 'v1', 'Node')): 17 | (length(items)): 1 18 | - error: 19 | resource: 20 | (x_k8s_list($client, 'v1', 'Node')): 21 | (length(items)): 2 22 | -------------------------------------------------------------------------------- /testdata/e2e/examples/template/README.md: -------------------------------------------------------------------------------- 1 | # Test: `template` 2 | 3 | *No description* 4 | 5 | ## Bindings 6 | 7 | | # | Name | Value | 8 | |:-:|---|---| 9 | | 1 | `foo` | "(join('-', [$namespace, 'foo']))" | 10 | 11 | ## Steps 12 | 13 | | # | Name | Bindings | Try | Catch | Finally | 14 | |:-:|---|:-:|:-:|:-:|:-:| 15 | | 1 | [step-1](#step-step-1) | 0 | 3 | 0 | 0 | 16 | 17 | ### Step: `step-1` 18 | 19 | *No description* 20 | 21 | #### Try 22 | 23 | | # | Operation | Bindings | Outputs | Description | 24 | |:-:|---|:-:|:-:|---| 25 | | 1 | `apply` | 0 | 0 | *No description* | 26 | | 2 | `assert` | 0 | 0 | *No description* | 27 | | 3 | `delete` | 0 | 0 | *No description* | 28 | 29 | --- 30 | 31 | -------------------------------------------------------------------------------- /pkg/client/patch.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "k8s.io/apimachinery/pkg/api/meta" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ) 9 | 10 | func PatchObject(actual, expected runtime.Object) (runtime.Object, error) { 11 | if actual == nil || expected == nil { 12 | return nil, errors.New("actual and expected objects must not be nil") 13 | } 14 | actualMeta, err := meta.Accessor(actual) 15 | if err != nil { 16 | return nil, err 17 | } 18 | copy := expected.DeepCopyObject() 19 | expectedMeta, err := meta.Accessor(copy) 20 | if err != nil { 21 | return nil, err 22 | } 23 | expectedMeta.SetResourceVersion(actualMeta.GetResourceVersion()) 24 | return copy, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/runner/flags/flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 7 | ) 8 | 9 | func GetFlags(config v1alpha1.ConfigurationSpec) map[string]string { 10 | flags := map[string]string{ 11 | "test.v": "true", 12 | "test.paniconexit0": "true", 13 | "test.fullpath": "false", 14 | "test.run": config.IncludeTestRegex, 15 | "test.skip": config.ExcludeTestRegex, 16 | } 17 | if config.Parallel != nil { 18 | flags["test.parallel"] = strconv.Itoa(*config.Parallel) 19 | } 20 | if config.RepeatCount != nil { 21 | flags["test.count"] = strconv.Itoa(*config.RepeatCount) 22 | } 23 | return flags 24 | } 25 | -------------------------------------------------------------------------------- /pkg/runner/summary/summary_test.go: -------------------------------------------------------------------------------- 1 | package summary 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSummay(t *testing.T) { 11 | var wg sync.WaitGroup 12 | var s Summary 13 | const count int32 = 10000 14 | for i := 0; i < int(count); i++ { 15 | wg.Add(3) 16 | go func() { 17 | defer wg.Done() 18 | s.IncFailed() 19 | }() 20 | go func() { 21 | defer wg.Done() 22 | s.IncPassed() 23 | }() 24 | go func() { 25 | defer wg.Done() 26 | s.IncSkipped() 27 | }() 28 | } 29 | wg.Wait() 30 | assert.Equal(t, count, s.Failed()) 31 | assert.Equal(t, count, s.Passed()) 32 | assert.Equal(t, count, s.Skipped()) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/validation/test/binding.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateBinding(path *field.Path, obj v1alpha1.Binding) field.ErrorList { 9 | var errs field.ErrorList 10 | if err := obj.CheckName(); err != nil { 11 | errs = append(errs, field.Invalid(path.Child("name"), obj.Name, err.Error())) 12 | } 13 | return errs 14 | } 15 | 16 | func ValidateBindings(path *field.Path, objs ...v1alpha1.Binding) field.ErrorList { 17 | var errs field.ErrorList 18 | for i, obj := range objs { 19 | errs = append(errs, ValidateBinding(path.Index(i), obj)...) 20 | } 21 | return errs 22 | } 23 | -------------------------------------------------------------------------------- /testdata/commands/test/with_test_dirs.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: chainsaw-test 4 | - TestDirs [.. .] 5 | - SkipDelete false 6 | - FailFast false 7 | - ReportFormat '' 8 | - ReportName 'chainsaw-report' 9 | - Namespace '' 10 | - FullName false 11 | - IncludeTestRegex '' 12 | - ExcludeTestRegex '' 13 | - ApplyTimeout 5s 14 | - AssertTimeout 30s 15 | - CleanupTimeout 30s 16 | - DeleteTimeout 15s 17 | - ErrorTimeout 30s 18 | - ExecTimeout 5s 19 | - NoCluster false 20 | - PauseOnFailure false 21 | Loading tests... 22 | Loading values... 23 | Running tests... 24 | Tests Summary... 25 | - Passed tests 0 26 | - Failed tests 0 27 | - Skipped tests 0 28 | Done. 29 | -------------------------------------------------------------------------------- /website/docs/collectors/index.md: -------------------------------------------------------------------------------- 1 | # Collectors 2 | 3 | ## Purpose 4 | 5 | The purpose of collectors is to collect certain information about the outcome of a step should it fail (in the case of `catch`) or at the end of the step (in the case of `finally`). 6 | 7 | The ultimate goal of collectors is to gather information about the failure of a step and therefore help understand what caused it to fail. 8 | 9 | A test step can have an arbitrary number of collectors. 10 | 11 | ## Available collectors 12 | 13 | - [Pod logs](./pod-logs.md) 14 | - [Events](./events.md) 15 | - [Get](./get.md) 16 | - [Describe](./describe.md) 17 | 18 | ## Templating 19 | 20 | All `string` fields in collectors support templating. -------------------------------------------------------------------------------- /website/docs/configuration/selector.md: -------------------------------------------------------------------------------- 1 | # Label selectors 2 | 3 | Chainsaw can filter the tests to run using [label selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors). 4 | 5 | You can pass label selectors using the `--selector` flag when invoking the `chainsaw test` command. 6 | 7 | ## Example 8 | 9 | Given the test below: 10 | 11 | ```yaml 12 | apiVersion: chainsaw.kyverno.io/v1alpha1 13 | kind: Test 14 | metadata: 15 | name: basic 16 | labels: 17 | foo: bar 18 | spec: 19 | # ... 20 | ``` 21 | 22 | Invoking Chainsaw with the command below will take the test above into account: 23 | 24 | ```bash 25 | chainsaw test --selector foo=bar 26 | ``` 27 | -------------------------------------------------------------------------------- /testdata/commands/test/with_regex.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: chainsaw-test 4 | - TestDirs [.] 5 | - SkipDelete false 6 | - FailFast false 7 | - ReportFormat '' 8 | - ReportName 'chainsaw-report' 9 | - Namespace '' 10 | - FullName false 11 | - IncludeTestRegex 'test[4-6]' 12 | - ExcludeTestRegex 'test[1-3]' 13 | - ApplyTimeout 5s 14 | - AssertTimeout 30s 15 | - CleanupTimeout 30s 16 | - DeleteTimeout 15s 17 | - ErrorTimeout 30s 18 | - ExecTimeout 5s 19 | - NoCluster false 20 | - PauseOnFailure false 21 | Loading tests... 22 | Loading values... 23 | Running tests... 24 | Tests Summary... 25 | - Passed tests 0 26 | - Failed tests 0 27 | - Skipped tests 0 28 | Done. 29 | -------------------------------------------------------------------------------- /testdata/e2e/examples/inline/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: inline 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | resource: 11 | apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: quick-start 15 | data: 16 | foo: bar 17 | - assert: 18 | resource: 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: quick-start 23 | data: 24 | foo: bar 25 | -------------------------------------------------------------------------------- /testdata/commands/test/with_suppress.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: chainsaw-test 4 | - Timeout 30s 5 | - TestDirs [.] 6 | - SkipDelete false 7 | - FailFast false 8 | - ReportFormat '' 9 | - ReportName 'chainsaw-report' 10 | - Namespace '' 11 | - FullName false 12 | - IncludeTestRegex '' 13 | - ExcludeTestRegex '' 14 | - ApplyTimeout 5s 15 | - AssertTimeout 30s 16 | - CleanupTimeout 30s 17 | - DeleteTimeout 15s 18 | - ErrorTimeout 30s 19 | - ExecTimeout 5s 20 | - NoCluster false 21 | - PauseOnFailure false 22 | Loading tests... 23 | Loading values... 24 | Running tests... 25 | Tests Summary... 26 | - Passed tests 0 27 | - Failed tests 0 28 | - Skipped tests 0 29 | Done. 30 | -------------------------------------------------------------------------------- /testdata/e2e/examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Test: `basic` 2 | 3 | This is a very simple test that creates a configmap and checks the content is as expected. 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 3 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | This steps applies the configmap in the cluster and checks the configmap content. 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | Create the configmap. | 20 | | 2 | `assert` | 0 | 0 | Check the configmap content. | 21 | | 3 | `script` | 0 | 0 | *No description* | 22 | 23 | --- 24 | 25 | -------------------------------------------------------------------------------- /testdata/e2e/examples/update-crd/README.md: -------------------------------------------------------------------------------- 1 | # Test: `update-crd` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 6 | 0 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | | 3 | `apply` | 0 | 0 | *No description* | 22 | | 4 | `update` | 0 | 0 | *No description* | 23 | | 5 | `error` | 0 | 0 | *No description* | 24 | | 6 | `assert` | 0 | 0 | *No description* | 25 | 26 | --- 27 | 28 | -------------------------------------------------------------------------------- /.release-notes/main.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `TODO`. 4 | 5 | 16 | 17 | ## ‼️ Breaking changes ‼️ 18 | 19 | - Resource templating is now enabled by default 20 | 21 | ## 💫 New features 💫 22 | 23 | - Added `--pause-on-failure` flag to pause when a test failure happens (to ease troubleshooting) 24 | - Improved cleanup management logic, alternating `catch`, `finally` and `@cleanup` per step 25 | 26 | ## 🔧 Fixes 🔧 27 | 28 | - Fixed issue with cluster incorrectly registered 29 | - Force background deletion propagation policy (useful when testing unmanaged `Job`) 30 | -------------------------------------------------------------------------------- /testdata/commands/test/with_repeat_count.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: chainsaw-test 4 | - TestDirs [.] 5 | - SkipDelete false 6 | - FailFast false 7 | - ReportFormat '' 8 | - ReportName 'chainsaw-report' 9 | - Namespace '' 10 | - FullName false 11 | - IncludeTestRegex '' 12 | - ExcludeTestRegex '' 13 | - ApplyTimeout 5s 14 | - AssertTimeout 30s 15 | - CleanupTimeout 30s 16 | - DeleteTimeout 15s 17 | - ErrorTimeout 30s 18 | - ExecTimeout 5s 19 | - RepeatCount 3 20 | - NoCluster false 21 | - PauseOnFailure false 22 | Loading tests... 23 | Loading values... 24 | Running tests... 25 | Tests Summary... 26 | - Passed tests 0 27 | - Failed tests 0 28 | - Skipped tests 0 29 | Done. 30 | -------------------------------------------------------------------------------- /pkg/validation/test/script.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateScript(path *field.Path, obj *v1alpha1.Script) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Content == "" { 12 | errs = append(errs, field.Invalid(path.Child("content"), obj, "content must be specified")) 13 | } 14 | errs = append(errs, ValidateCheck(path.Child("check"), obj.Check)...) 15 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 16 | errs = append(errs, ValidateOutputs(path.Child("outputs"), obj.Outputs...)...) 17 | } 18 | return errs 19 | } 20 | -------------------------------------------------------------------------------- /testdata/e2e/examples/catch/README.md: -------------------------------------------------------------------------------- 1 | # Test: `catch` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 2 | 0 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | 22 | #### Catch 23 | 24 | | # | Operation | Bindings | Outputs | Description | 25 | |:-:|---|:-:|:-:|---| 26 | | 1 | `events` | 0 | 0 | *No description* | 27 | | 2 | `describe` | 0 | 0 | *No description* | 28 | 29 | --- 30 | 31 | -------------------------------------------------------------------------------- /testdata/e2e/examples/finally/README.md: -------------------------------------------------------------------------------- 1 | # Test: `finally` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 0 | 2 | 10 | 11 | ### Step: `step-1` 12 | 13 | *No description* 14 | 15 | #### Try 16 | 17 | | # | Operation | Bindings | Outputs | Description | 18 | |:-:|---|:-:|:-:|---| 19 | | 1 | `apply` | 0 | 0 | *No description* | 20 | | 2 | `assert` | 0 | 0 | *No description* | 21 | 22 | #### Finally 23 | 24 | | # | Operation | Bindings | Outputs | Description | 25 | |:-:|---|:-:|:-:|---| 26 | | 1 | `events` | 0 | 0 | *No description* | 27 | | 2 | `script` | 0 | 0 | *No description* | 28 | 29 | --- 30 | 31 | -------------------------------------------------------------------------------- /website/docs/operations/sleep.md: -------------------------------------------------------------------------------- 1 | # Sleep 2 | 3 | The `sleep` operation provides a means to sleep for a configured duration. 4 | 5 | ## Configuration 6 | 7 | !!! tip "Reference documentation" 8 | The full structure of the `Sleep` is documented [here](../apis/chainsaw.v1alpha1.md#chainsaw-kyverno-io-v1alpha1-Sleep). 9 | 10 | ## Usage examples 11 | 12 | Below is an example of using `sleep` in a `Test` resource. 13 | 14 | !!! example 15 | 16 | ```yaml 17 | apiVersion: chainsaw.kyverno.io/v1alpha1 18 | kind: Test 19 | metadata: 20 | name: example 21 | spec: 22 | steps: 23 | - try: 24 | # ... 25 | - sleep: 26 | duration: 30s 27 | # ... 28 | ``` 29 | -------------------------------------------------------------------------------- /pkg/validation/test/command.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateCommand(path *field.Path, obj *v1alpha1.Command) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj != nil { 11 | if obj.Entrypoint == "" { 12 | errs = append(errs, field.Invalid(path.Child("entrypoint"), obj, "entrypoint must be specified")) 13 | } 14 | errs = append(errs, ValidateCheck(path.Child("check"), obj.Check)...) 15 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 16 | errs = append(errs, ValidateOutputs(path.Child("outputs"), obj.Outputs...)...) 17 | } 18 | return errs 19 | } 20 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/binding.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | var identifier = regexp.MustCompile(`^(?:\w+|\(.+\))$`) 9 | 10 | // Binding represents a key/value set as a binding in an executing test. 11 | type Binding struct { 12 | // Name the name of the binding. 13 | // +kubebuilder:validation:Pattern=`^(?:\w+|\(.+\))$` 14 | Name string `json:"name"` 15 | 16 | // Value value of the binding. 17 | // +kubebuilder:validation:Schemaless 18 | // +kubebuilder:pruning:PreserveUnknownFields 19 | Value Any `json:"value"` 20 | } 21 | 22 | func (b Binding) CheckName() error { 23 | if !identifier.MatchString(b.Name) { 24 | return fmt.Errorf("invalid name %s", b.Name) 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/runner/mutate/convert.go: -------------------------------------------------------------------------------- 1 | package mutate 2 | 3 | func convertSlice(in any) []any { 4 | data, ok := in.([]any) 5 | if !ok { 6 | return nil 7 | } 8 | out := []any{} 9 | for _, v := range data { 10 | out = append(out, convert(v)) 11 | } 12 | return out 13 | } 14 | 15 | func convertMap(in any) map[string]any { 16 | data, ok := in.(map[any]any) 17 | if !ok { 18 | return nil 19 | } 20 | out := map[string]any{} 21 | for k, v := range data { 22 | out[k.(string)] = convert(v) 23 | } 24 | return out 25 | } 26 | 27 | func convert(in any) any { 28 | if result := convertSlice(in); result != nil { 29 | return result 30 | } 31 | if result := convertMap(in); result != nil { 32 | return result 33 | } 34 | return in 35 | } 36 | -------------------------------------------------------------------------------- /testdata/e2e/examples/script-env/README.md: -------------------------------------------------------------------------------- 1 | # Test: `script-env` 2 | 3 | *No description* 4 | 5 | ## Bindings 6 | 7 | | # | Name | Value | 8 | |:-:|---|---| 9 | | 1 | `chainsaw` | "chainsaw" | 10 | 11 | ## Steps 12 | 13 | | # | Name | Bindings | Try | Catch | Finally | 14 | |:-:|---|:-:|:-:|:-:|:-:| 15 | | 1 | [step-1](#step-step-1) | 1 | 2 | 0 | 0 | 16 | 17 | ### Step: `step-1` 18 | 19 | *No description* 20 | 21 | #### Bindings 22 | 23 | | # | Name | Value | 24 | |:-:|---|---| 25 | | 1 | `hello` | "hello" | 26 | 27 | #### Try 28 | 29 | | # | Operation | Bindings | Outputs | Description | 30 | |:-:|---|:-:|:-:|---| 31 | | 1 | `script` | 0 | 0 | *No description* | 32 | | 2 | `command` | 0 | 0 | *No description* | 33 | 34 | --- 35 | 36 | -------------------------------------------------------------------------------- /pkg/client/unstructured_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/utils/ptr" 9 | ) 10 | 11 | func TestToUnstructured(t *testing.T) { 12 | var nilPtr *int 13 | assert.Panics(t, func() { 14 | ToUnstructured(nilPtr) 15 | }) 16 | assert.Equal(t, ToUnstructured(ptr.To(Namespace("foo"))), unstructured.Unstructured{ 17 | Object: map[string]any{ 18 | "apiVersion": "v1", 19 | "kind": "Namespace", 20 | "metadata": map[string]any{ 21 | "creationTimestamp": nil, 22 | "name": "foo", 23 | }, 24 | "spec": map[string]any{}, 25 | "status": map[string]any{}, 26 | }, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /testdata/commands/lint/configuration/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "chainsaw.kyverno.io/v1alpha1", 3 | "kind": "Configuration", 4 | "metadata": { 5 | "name": "valid", 6 | "namespace": "kyverno" 7 | }, 8 | "spec": { 9 | "timeouts": { 10 | "apply": "5s", 11 | "assert": "10s", 12 | "error": "10s", 13 | "delete": "5s", 14 | "cleanup": "5s", 15 | "exec": "10s" 16 | }, 17 | "skipDelete": true, 18 | "failFast": true, 19 | "parallel": 5, 20 | "reportFormat": "JSON", 21 | "reportName": "custom-chainsaw-report", 22 | "namespace": "test-namespace", 23 | "fullName": true, 24 | "includeTestRegex": "^include-.*", 25 | "excludeTestRegex": "^exclude-.*" 26 | } 27 | } -------------------------------------------------------------------------------- /website/docs/more/lint.md: -------------------------------------------------------------------------------- 1 | # Lint tests 2 | 3 | ## Overview 4 | 5 | Chainsaw comes with a `lint` command to detect ill-formated tests. 6 | 7 | !!! tip "Reference documentation" 8 | 9 | You can view the full command documentation [here](../commands/chainsaw_lint.md). 10 | 11 | ## Usage 12 | 13 | To build the docs of a test, Chainsaw provides the `chainsaw lint test -f path/to/chainsaw-test.yaml` command. 14 | 15 | ```bash 16 | chainsaw lint test -f - < Note: The reportPath can be specified as either a relative or an absolute path. 29 | -------------------------------------------------------------------------------- /testdata/commands/test/all_flags.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading default configuration... 3 | - Using test file: custom-test.yaml 4 | - TestDirs [.] 5 | - SkipDelete false 6 | - FailFast false 7 | - ReportFormat 'XML' 8 | - ReportName 'foo' 9 | - Namespace 'bar' 10 | - FullName true 11 | - IncludeTestRegex '^.*$' 12 | - ExcludeTestRegex '^.*$' 13 | - ApplyTimeout 1m40s 14 | - AssertTimeout 1m40s 15 | - CleanupTimeout 1m40s 16 | - DeleteTimeout 1m40s 17 | - ErrorTimeout 1m40s 18 | - ExecTimeout 1m40s 19 | - Parallel 24 20 | - RepeatCount 12 21 | - ForceTerminationGracePeriod 5s 22 | - NoCluster false 23 | - PauseOnFailure false 24 | Loading tests... 25 | Loading values... 26 | Running tests... 27 | Tests Summary... 28 | - Passed tests 0 29 | - Failed tests 0 30 | - Skipped tests 0 31 | Done. 32 | -------------------------------------------------------------------------------- /testdata/e2e/examples/catch/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: catch 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | resource: 11 | apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: quick-start 15 | data: 16 | foo: bar 17 | - assert: 18 | resource: 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: quick-start 23 | data: 24 | foo: bar 25 | catch: 26 | - events: {} 27 | - describe: 28 | resource: crds 29 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/object_selector.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // ObjectSelector represents a strategy to select objects. 4 | // For a single object name and namespace are used to identify the object. 5 | // For multiple objects use labels. 6 | type ObjectSelector struct { 7 | // Namespace of the referent. 8 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 9 | // +optional 10 | Namespace string `json:"namespace,omitempty"` 11 | 12 | // Name of the referent. 13 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 14 | // +optional 15 | Name string `json:"name,omitempty"` 16 | 17 | // Label selector to match objects to delete 18 | // +optional 19 | Labels map[string]string `json:"labels,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /testdata/test/custom-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: chainsaw.kyverno.io/v1alpha1 2 | kind: Test 3 | metadata: 4 | name: custom-test 5 | spec: 6 | skip: false 7 | concurrent: true 8 | skipDelete: true 9 | namespace: "custom-namespace-for-test" 10 | steps: 11 | - name: "test-step-initial-setup" 12 | timeouts: 13 | apply: 5s 14 | skipDelete: false 15 | try: 16 | - apply: 17 | file: /configs/service-config.yaml 18 | timeout: 45s 19 | continueOnError: true 20 | - name: "test-step-apply-changes" 21 | try: 22 | - apply: 23 | file: /configs/service-config.yaml 24 | timeout: 45s 25 | continueOnError: true 26 | - assert: 27 | file: /assertions/verify-service.yaml 28 | timeout: 30s 29 | continueOnError: false 30 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/object_labels_selector.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // ObjectLabelsSelector represents a strategy to select objects. 4 | // For a single object name and namespace are used to identify the object. 5 | // For multiple objects use selector. 6 | type ObjectLabelsSelector struct { 7 | // Namespace of the referent. 8 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 9 | // +optional 10 | Namespace string `json:"namespace,omitempty"` 11 | 12 | // Name of the referent. 13 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 14 | // +optional 15 | Name string `json:"name,omitempty"` 16 | 17 | // Selector defines labels selector. 18 | // +optional 19 | Selector string `json:"selector,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /testdata/commands/test/config_all_fields.txt: -------------------------------------------------------------------------------- 1 | Version: --- 2 | Loading config (../../../testdata/commands/test/config/config_all_fields.yaml)... 3 | - Using test file: chainsaw-test 4 | - TestDirs [.] 5 | - SkipDelete true 6 | - FailFast true 7 | - ReportFormat 'JSON' 8 | - ReportName 'custom-chainsaw-report' 9 | - Namespace 'test-namespace' 10 | - FullName true 11 | - IncludeTestRegex '^include-.*' 12 | - ExcludeTestRegex '^exclude-.*' 13 | - ApplyTimeout 5s 14 | - AssertTimeout 10s 15 | - CleanupTimeout 5s 16 | - DeleteTimeout 5s 17 | - ErrorTimeout 10s 18 | - ExecTimeout 10s 19 | - Parallel 5 20 | - NoCluster false 21 | - PauseOnFailure false 22 | Loading tests... 23 | Loading values... 24 | Running tests... 25 | Tests Summary... 26 | - Passed tests 0 27 | - Failed tests 0 28 | - Skipped tests 0 29 | Done. 30 | -------------------------------------------------------------------------------- /testdata/e2e/examples/list/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: list 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | file: list.yaml 11 | - assert: 12 | resource: 13 | apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: cm-1 17 | namespace: default 18 | data: 19 | key: 'yes' 20 | - assert: 21 | resource: 22 | apiVersion: v1 23 | kind: ConfigMap 24 | metadata: 25 | name: cm-2 26 | namespace: default 27 | data: 28 | key: 'no' 29 | -------------------------------------------------------------------------------- /website/docs/configuration/cleanup-delay.md: -------------------------------------------------------------------------------- 1 | # Delay before cleanup 2 | 3 | At the end of each test, Chainsaw will delete the resources it created during the test. 4 | 5 | When testing operators, it can be useful to wait a little bit before starting the cleanup process to make sure the operator/controller has the necessary time to update the internal state. 6 | 7 | For this reason, Chainsaw provides the `delayBeforeCleanup` configuration option and the corresponding `--delay-before-cleanup` flag. 8 | 9 | ## Configuration 10 | 11 | ```yaml 12 | apiVersion: chainsaw.kyverno.io/v1alpha1 13 | kind: Configuration 14 | metadata: 15 | name: custom-config 16 | spec: 17 | # ... 18 | delayBeforeCleanup: 5s 19 | # ... 20 | ``` 21 | 22 | ## Flag 23 | 24 | ```bash 25 | chainsaw test --delay-before-cleanup 5s ... 26 | ``` 27 | -------------------------------------------------------------------------------- /pkg/client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/client-go/rest" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | cfg *rest.Config 14 | wantErr bool 15 | }{{ 16 | name: "nil config", 17 | cfg: nil, 18 | wantErr: true, 19 | }, { 20 | name: "valid config", 21 | cfg: &rest.Config{ 22 | Host: "http://localhost", 23 | APIPath: "/api", 24 | }, 25 | wantErr: false, 26 | }} 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | got, err := New(tt.cfg) 30 | if tt.wantErr { 31 | assert.Error(t, err) 32 | assert.Nil(t, got) 33 | } else { 34 | assert.NoError(t, err) 35 | assert.NotNil(t, got) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/runner/operations/internal/command_output.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/kyverno/chainsaw/pkg/runner/logging" 9 | ) 10 | 11 | type CommandOutput struct { 12 | Stdout bytes.Buffer 13 | Stderr bytes.Buffer 14 | } 15 | 16 | func (c *CommandOutput) Out() string { 17 | return strings.TrimSpace(c.Stdout.String()) 18 | } 19 | 20 | func (c *CommandOutput) Err() string { 21 | return strings.TrimSpace(c.Stderr.String()) 22 | } 23 | 24 | func (c *CommandOutput) Sections() []fmt.Stringer { 25 | var sections []fmt.Stringer 26 | o := c.Out() 27 | e := c.Err() 28 | if o != "" { 29 | sections = append(sections, logging.Section("STDOUT", o)) 30 | } 31 | if e != "" { 32 | sections = append(sections, logging.Section("STDERR", e)) 33 | } 34 | return sections 35 | } 36 | -------------------------------------------------------------------------------- /testdata/e2e/examples/template/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: template 6 | spec: 7 | template: true 8 | bindings: 9 | - name: foo 10 | value: (join('-', [$namespace, 'foo'])) 11 | steps: 12 | - try: 13 | - apply: 14 | resource: 15 | apiVersion: v1 16 | kind: ConfigMap 17 | metadata: 18 | name: ($foo) 19 | - assert: 20 | resource: 21 | apiVersion: v1 22 | kind: ConfigMap 23 | metadata: 24 | name: ($foo) 25 | - delete: 26 | ref: 27 | apiVersion: v1 28 | kind: ConfigMap 29 | name: ($foo) 30 | -------------------------------------------------------------------------------- /testdata/e2e/examples/finally/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: finally 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | resource: 11 | apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: quick-start 15 | data: 16 | foo: bar 17 | - assert: 18 | resource: 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: quick-start 23 | data: 24 | foo: bar 25 | finally: 26 | - events: 27 | timeout: 30s 28 | format: yaml 29 | - script: 30 | content: echo goodbye 31 | -------------------------------------------------------------------------------- /pkg/runner/bindings/string.go: -------------------------------------------------------------------------------- 1 | package bindings 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/jmespath-community/go-jmespath/pkg/binding" 8 | "github.com/kyverno/chainsaw/pkg/mutate" 9 | "github.com/kyverno/chainsaw/pkg/runner/functions" 10 | "github.com/kyverno/kyverno-json/pkg/engine/template" 11 | ) 12 | 13 | func String(in string, bindings binding.Bindings) (string, error) { 14 | if in == "" { 15 | return "", nil 16 | } 17 | ctx := context.TODO() 18 | if converted, err := mutate.Mutate(ctx, nil, mutate.Parse(ctx, in), nil, bindings, template.WithFunctionCaller(functions.Caller)); err != nil { 19 | return "", err 20 | } else { 21 | if converted, ok := converted.(string); !ok { 22 | return "", fmt.Errorf("expression didn't evaluate to a string (%s)", in) 23 | } else { 24 | return converted, nil 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/e2e/examples/non-resource-assertion/README.md: -------------------------------------------------------------------------------- 1 | # Test: `non-resource-assertion` 2 | 3 | *No description* 4 | 5 | ## Steps 6 | 7 | | # | Name | Bindings | Try | Catch | Finally | 8 | |:-:|---|:-:|:-:|:-:|:-:| 9 | | 1 | [step-1](#step-step-1) | 0 | 2 | 0 | 0 | 10 | | 2 | [step-2](#step-step-2) | 0 | 2 | 0 | 0 | 11 | 12 | ### Step: `step-1` 13 | 14 | *No description* 15 | 16 | #### Try 17 | 18 | | # | Operation | Bindings | Outputs | Description | 19 | |:-:|---|:-:|:-:|---| 20 | | 1 | `assert` | 0 | 0 | *No description* | 21 | | 2 | `error` | 0 | 0 | *No description* | 22 | 23 | ### Step: `step-2` 24 | 25 | *No description* 26 | 27 | #### Try 28 | 29 | | # | Operation | Bindings | Outputs | Description | 30 | |:-:|---|:-:|:-:|---| 31 | | 1 | `assert` | 0 | 0 | *No description* | 32 | | 2 | `error` | 0 | 0 | *No description* | 33 | 34 | --- 35 | 36 | -------------------------------------------------------------------------------- /pkg/values/load.go: -------------------------------------------------------------------------------- 1 | package values 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | mapsutils "github.com/kyverno/chainsaw/pkg/utils/maps" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | func Load(paths ...string) (map[string]any, error) { 14 | base := map[string]any{} 15 | for _, path := range paths { 16 | currentMap := map[string]any{} 17 | bytes, err := readFile(path) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 22 | return nil, fmt.Errorf("failed to parse %s (%w)", path, err) 23 | } 24 | base = mapsutils.Merge(base, currentMap) 25 | } 26 | return base, nil 27 | } 28 | 29 | func readFile(filePath string) ([]byte, error) { 30 | if strings.TrimSpace(filePath) == "-" { 31 | return io.ReadAll(os.Stdin) 32 | } 33 | return os.ReadFile(filePath) 34 | } 35 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_completion_powershell.md: -------------------------------------------------------------------------------- 1 | ## chainsaw completion powershell 2 | 3 | Generate the autocompletion script for powershell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for powershell. 8 | 9 | To load completions in your current shell session: 10 | 11 | chainsaw completion powershell | Out-String | Invoke-Expression 12 | 13 | To load completions for every new session, add the output of the above command 14 | to your powershell profile. 15 | 16 | 17 | ``` 18 | chainsaw completion powershell [flags] 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | -h, --help help for powershell 25 | --no-descriptions disable completion descriptions 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [chainsaw completion](chainsaw_completion.md) - Generate the autocompletion script for the specified shell 31 | 32 | -------------------------------------------------------------------------------- /website/docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | Resources, blog posts and videos talking about Chainsaw: 4 | 5 | - [Kyverno Chainsaw - The ultimate end-to-end testing tool!](https://kyverno.io/blog/2023/12/12/kyverno-chainsaw-the-ultimate-end-to-end-testing-tool/) 6 | - [Kyverno Chainsaw - Exploring the Power of Assertion Trees!](https://kyverno.io/blog/2023/12/13/kyverno-chainsaw-exploring-the-power-of-assertion-trees/) 7 | - [Nirmata Office Hours for Kyverno- Episode 9- Demonstrate Kyverno Chainsaw](https://www.youtube.com/watch?v=IrIteTTjlbU) 8 | - [Kubebuilder Community Meeting - February 1, 2024](https://www.youtube.com/watch?v=Ejof-wtAdQM) 9 | - [Kyverno Chainsaw 0.1.4 - Awesome new features!](https://kyverno.io/blog/2024/02/15/kyverno-chainsaw-0.1.4-awesome-new-features/) 10 | - [Mastering Kubernetes Testing with Kyverno Chainsaw!](https://youtu.be/hQJWGzogIiI) 11 | -------------------------------------------------------------------------------- /testdata/resource/custom-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: 'nginx' 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: 'nginx' 10 | serviceName: "'nginx'" 11 | replicas: 3 12 | template: 13 | metadata: 14 | labels: 15 | app: 'nginx' 16 | spec: 17 | terminationGracePeriodSeconds: 10 18 | containers: 19 | - name: 'nginx' 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | name: web 24 | volumeMounts: 25 | - name: www 26 | mountPath: /usr/share/nginx/html 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: www 30 | spec: 31 | accessModes: [ "ReadWriteOnce" ] 32 | storageClassName: "my-storage-class" 33 | resources: 34 | requests: 35 | storage: 1Gi -------------------------------------------------------------------------------- /pkg/client/key.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kyverno/pkg/ext/output/color" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | func ObjectKey(obj metav1.Object) ctrlclient.ObjectKey { 12 | return ctrlclient.ObjectKey{ 13 | Name: obj.GetName(), 14 | Namespace: obj.GetNamespace(), 15 | } 16 | } 17 | 18 | func Name(key ctrlclient.ObjectKey) string { 19 | return ColouredName(key, nil) 20 | } 21 | 22 | func ColouredName(key ctrlclient.ObjectKey, color *color.Color) string { 23 | sprint := fmt.Sprint 24 | if color != nil { 25 | sprint = color.Sprint 26 | } 27 | name := key.Name 28 | if name == "" { 29 | name = "*" 30 | } 31 | name = sprint(name) 32 | if key.Namespace != "" { 33 | name = sprint(key.Namespace) + "/" + name 34 | } 35 | return name 36 | } 37 | -------------------------------------------------------------------------------- /testdata/resource/folder-valid/custom-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: 'nginx' 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: 'nginx' 10 | serviceName: "'nginx'" 11 | replicas: 3 12 | template: 13 | metadata: 14 | labels: 15 | app: 'nginx' 16 | spec: 17 | terminationGracePeriodSeconds: 10 18 | containers: 19 | - name: 'nginx' 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | name: web 24 | volumeMounts: 25 | - name: www 26 | mountPath: /usr/share/nginx/html 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: www 30 | spec: 31 | accessModes: [ "ReadWriteOnce" ] 32 | storageClassName: "my-storage-class" 33 | resources: 34 | requests: 35 | storage: 1Gi -------------------------------------------------------------------------------- /testdata/resource/folder-invalid/custom-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: 'nginx' 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: 'nginx' 10 | serviceName: "'nginx'" 11 | replicas: 3 12 | template: 13 | metadata: 14 | labels: 15 | app: 'nginx' 16 | spec: 17 | terminationGracePeriodSeconds: 10 18 | containers: 19 | - name: 'nginx' 20 | image: nginx 21 | ports: 22 | - containerPort: 80 23 | name: web 24 | volumeMounts: 25 | - name: www 26 | mountPath: /usr/share/nginx/html 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: www 30 | spec: 31 | accessModes: [ "ReadWriteOnce" ] 32 | storageClassName: "my-storage-class" 33 | resources: 34 | requests: 35 | storage: 1Gi -------------------------------------------------------------------------------- /website/docs/operations/non-resource-assert.md: -------------------------------------------------------------------------------- 1 | # Non-resource assertions 2 | 3 | Under certain circumstances, it makes sense to evaluate assertions that do not depend on resources. 4 | For example, when asserting the number of nodes in a cluster is equal to a known value. 5 | 6 | ## Usage examples 7 | 8 | Below is an example of using `assert` in a `Test` resource. 9 | 10 | !!! example "Using a file" 11 | 12 | ```yaml 13 | apiVersion: chainsaw.kyverno.io/v1alpha1 14 | kind: Test 15 | metadata: 16 | name: non-resource-assertion 17 | spec: 18 | steps: 19 | - try: 20 | - assert: 21 | resource: 22 | (x_k8s_list($client, 'v1', 'Node')): 23 | (length(items)): 1 24 | - error: 25 | resource: 26 | (x_k8s_list($client, 'v1', 'Node')): 27 | (length(items)): 2 28 | ``` 29 | -------------------------------------------------------------------------------- /pkg/validation/test/test_spec_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | func TestValidateTestSpec(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | path *field.Path 15 | obj v1alpha1.TestSpec 16 | want field.ErrorList 17 | }{{ 18 | name: "empty", 19 | }, { 20 | name: "invalid catch", 21 | obj: v1alpha1.TestSpec{ 22 | Catch: []v1alpha1.Catch{{}}, 23 | }, 24 | want: field.ErrorList{ 25 | field.Invalid(field.NewPath("catch").Index(0), v1alpha1.Catch{}, "no statement found in operation"), 26 | }, 27 | }} 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | got := ValidateTestSpec(tt.path, tt.obj) 31 | assert.Equal(t, tt.want, got) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_completion_fish.md: -------------------------------------------------------------------------------- 1 | ## chainsaw completion fish 2 | 3 | Generate the autocompletion script for fish 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the fish shell. 8 | 9 | To load completions in your current shell session: 10 | 11 | chainsaw completion fish | source 12 | 13 | To load completions for every new session, execute once: 14 | 15 | chainsaw completion fish > ~/.config/fish/completions/chainsaw.fish 16 | 17 | You will need to start a new shell for this setup to take effect. 18 | 19 | 20 | ``` 21 | chainsaw completion fish [flags] 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | -h, --help help for fish 28 | --no-descriptions disable completion descriptions 29 | ``` 30 | 31 | ### SEE ALSO 32 | 33 | * [chainsaw completion](chainsaw_completion.md) - Generate the autocompletion script for the specified shell 34 | 35 | -------------------------------------------------------------------------------- /pkg/runner/env/expand_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestExpand(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | env map[string]string 13 | in []string 14 | want []string 15 | }{{ 16 | name: "nil", 17 | env: nil, 18 | in: []string{"echo", "$NAMESPACE"}, 19 | want: []string{"echo", "$NAMESPACE"}, 20 | }, { 21 | name: "empty", 22 | env: map[string]string{}, 23 | in: []string{"echo", "$NAMESPACE"}, 24 | want: []string{"echo", "$NAMESPACE"}, 25 | }, { 26 | name: "expand", 27 | env: map[string]string{"NAMESPACE": "foo"}, 28 | in: []string{"echo", "$NAMESPACE"}, 29 | want: []string{"echo", "foo"}, 30 | }} 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | got := Expand(tt.env, tt.in...) 34 | assert.Equal(t, tt.want, got) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/runner/timeout/timeout_test.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestGet(t *testing.T) { 12 | operation := metav1.Duration{Duration: 4 * time.Minute} 13 | fallback := 10 * time.Second 14 | tests := []struct { 15 | name string 16 | fallback time.Duration 17 | operation *metav1.Duration 18 | want *time.Duration 19 | }{{ 20 | name: "fallback", 21 | fallback: fallback, 22 | operation: nil, 23 | want: &fallback, 24 | }, { 25 | name: "operation", 26 | fallback: fallback, 27 | operation: &operation, 28 | want: &operation.Duration, 29 | }} 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | got := Get(tt.operation, tt.fallback) 33 | assert.Equal(t, tt.want, got) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /website/docs/examples/non-resource-assertion.md: -------------------------------------------------------------------------------- 1 | # Non resource assertion 2 | 3 | This test example demonstrates how to perform assertions not based on resources. 4 | 5 | 1. Asserts that the number of nodes in the cluster is equal to 1 6 | 7 | ## Setup 8 | 9 | See [Setup docs](./index.md#setup) 10 | 11 | ## Manifests 12 | 13 | ### `assertions.yaml` 14 | 15 | ```yaml 16 | (x_k8s_list($client, 'v1', 'Node')): 17 | (length(items): 1 18 | ``` 19 | 20 | ## Test 21 | 22 | ### `chainsaw-test.yaml` 23 | 24 | ```yaml 25 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 26 | apiVersion: chainsaw.kyverno.io/v1alpha1 27 | kind: Test 28 | metadata: 29 | name: non-resource-assertion 30 | spec: 31 | steps: 32 | - try: 33 | - assert: 34 | file: assertions.yaml 35 | ``` 36 | 37 | ## Execute 38 | 39 | ```bash 40 | chainsaw test 41 | ``` 42 | -------------------------------------------------------------------------------- /pkg/validation/test/test_step_spec.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateTestStepSpec(path *field.Path, obj v1alpha1.TestStepSpec) field.ErrorList { 9 | var errs field.ErrorList 10 | if len(obj.Try) == 0 { 11 | errs = append(errs, field.Required(path.Child("try"), "try block cannot be empty")) 12 | } 13 | for i, try := range obj.Try { 14 | errs = append(errs, ValidateOperation(path.Child("try").Index(i), try)...) 15 | } 16 | for i, catch := range obj.Catch { 17 | errs = append(errs, ValidateCatch(path.Child("catch").Index(i), catch)...) 18 | } 19 | for i, finally := range obj.Finally { 20 | errs = append(errs, ValidateFinally(path.Child("finally").Index(i), finally)...) 21 | } 22 | errs = append(errs, ValidateBindings(path.Child("bindings"), obj.Bindings...)...) 23 | return errs 24 | } 25 | -------------------------------------------------------------------------------- /pkg/runner/check/check.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/jmespath-community/go-jmespath/pkg/binding" 8 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 9 | "github.com/kyverno/chainsaw/pkg/runner/functions" 10 | "github.com/kyverno/kyverno-json/pkg/engine/assert" 11 | "github.com/kyverno/kyverno-json/pkg/engine/template" 12 | "k8s.io/apimachinery/pkg/util/validation/field" 13 | ) 14 | 15 | func Check(ctx context.Context, obj any, bindings binding.Bindings, check *v1alpha1.Check) (field.ErrorList, error) { 16 | if check == nil { 17 | return nil, errors.New("check is null") 18 | } 19 | if check.Value == nil { 20 | return nil, errors.New("check value is null") 21 | } 22 | if bindings == nil { 23 | bindings = binding.NewBindings() 24 | } 25 | return assert.Assert(ctx, nil, assert.Parse(ctx, check.Value), obj, bindings, template.WithFunctionCaller(functions.Caller)) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/discovery/discovery.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | fsutils "github.com/kyverno/chainsaw/pkg/utils/fs" 5 | "k8s.io/apimachinery/pkg/labels" 6 | ) 7 | 8 | func DiscoverTests(fileName string, selector labels.Selector, paths ...string) ([]Test, error) { 9 | folders, err := fsutils.DiscoverFolders(paths...) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return discoverTests(fileName, selector, folders...) 14 | } 15 | 16 | func discoverTests(fileName string, selector labels.Selector, folders ...string) ([]Test, error) { 17 | if selector == nil { 18 | selector = labels.Everything() 19 | } 20 | var tests []Test 21 | for _, folder := range folders { 22 | t, err := LoadTest(fileName, folder) 23 | if err != nil { 24 | return nil, err 25 | } 26 | for _, t := range t { 27 | if selector.Matches(labels.Set(t.Labels)) { 28 | tests = append(tests, t) 29 | } 30 | } 31 | } 32 | return tests, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/utils/fs/discover.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | 9 | "go.uber.org/multierr" 10 | "k8s.io/apimachinery/pkg/util/sets" 11 | ) 12 | 13 | func DiscoverFolders(paths ...string) ([]string, error) { 14 | folders := sets.New[string]() 15 | var errors []error 16 | for _, path := range paths { 17 | _, err := os.Stat(path) 18 | if err != nil { 19 | errors = append(errors, fmt.Errorf("error checking path %s: %v", path, err)) 20 | continue 21 | } 22 | err = filepath.Walk(path, func(file string, info fs.FileInfo, err error) error { 23 | if err != nil { 24 | return err 25 | } 26 | if info.IsDir() { 27 | folders.Insert(file) 28 | } 29 | return nil 30 | }) 31 | if err != nil { 32 | errors = append(errors, err) 33 | } 34 | } 35 | if len(errors) > 0 { 36 | return nil, multierr.Combine(errors...) 37 | } 38 | return sets.List(folders), nil 39 | } 40 | -------------------------------------------------------------------------------- /testdata/e2e/examples/patch/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: patch 6 | spec: 7 | steps: 8 | - try: 9 | - apply: 10 | resource: 11 | apiVersion: v1 12 | kind: ConfigMap 13 | metadata: 14 | name: quick-start 15 | data: 16 | foo: bar 17 | - patch: 18 | resource: 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: quick-start 23 | data: 24 | lorem: ipsum 25 | - assert: 26 | resource: 27 | apiVersion: v1 28 | kind: ConfigMap 29 | metadata: 30 | name: quick-start 31 | data: 32 | foo: bar 33 | lorem: ipsum 34 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw_completion.md: -------------------------------------------------------------------------------- 1 | ## chainsaw completion 2 | 3 | Generate the autocompletion script for the specified shell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for chainsaw for the specified shell. 8 | See each sub-command's help for details on how to use the generated script. 9 | 10 | 11 | ### Options 12 | 13 | ``` 14 | -h, --help help for completion 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [chainsaw](chainsaw.md) - Stronger tool for e2e testing 20 | * [chainsaw completion bash](chainsaw_completion_bash.md) - Generate the autocompletion script for bash 21 | * [chainsaw completion fish](chainsaw_completion_fish.md) - Generate the autocompletion script for fish 22 | * [chainsaw completion powershell](chainsaw_completion_powershell.md) - Generate the autocompletion script for powershell 23 | * [chainsaw completion zsh](chainsaw_completion_zsh.md) - Generate the autocompletion script for zsh 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/codegen.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Verify codegen 4 | 5 | permissions: {} 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - main 11 | - release-* 12 | push: 13 | branches: 14 | - main 15 | - release-* 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | codegen-verify: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 27 | - name: Setup Go 28 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 29 | with: 30 | go-version-file: go.mod 31 | cache-dependency-path: go.sum 32 | - name: Verify codegen 33 | run: | 34 | set -e 35 | make verify-codegen 36 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## Maintainers 2 | 3 | Maintainers are approvers who have shown good technical judgment in guiding feature design & development, have displayed overall knowledge of the project and features in the project, and are nurturing and receptive to everyone in the community. 4 | 5 | | Maintainer | GitHub ID | Affiliation | 6 | |--------------------------|--------------------------------------------------------|---------------------------| 7 | | Charles-Edouard Brétéché | [@eddycharly](https://github.com/eddycharly) | Nirmata | 8 | | Shubham Gupta | [@shubham-cmyk](https://github.com/shubham-cmyk) | - | 9 | 10 | **Note**: Please refer to the [Project Governance](https://kyverno.io/community/#project-governance) for more information on the responsibilities and privileges of a maintainer in `kyverno/chainsaw`. 11 | -------------------------------------------------------------------------------- /website/docs/configuration/grace.md: -------------------------------------------------------------------------------- 1 | # Termination graceful period 2 | 3 | Some Kubernetes resources can take time before being stopped. For example, deleting a `Pod` can take time if the underlying container doesn't quit quickly enough. 4 | 5 | For this reason, Chainsaw provides the `forceTerminationGracePeriod` configuration option and the corresponding `--force-termination-grace-period` flag. If set, Chainsaw will override the `terminationGracePeriodSeconds` when working with the following resource kinds: 6 | 7 | - `Pod` 8 | - `Deployment` 9 | - `StatefulSet` 10 | - `DaemonSet` 11 | - `Job` 12 | - `CronJob` 13 | 14 | ## Configuration 15 | 16 | ```yaml 17 | apiVersion: chainsaw.kyverno.io/v1alpha1 18 | kind: Configuration 19 | metadata: 20 | name: custom-config 21 | spec: 22 | # ... 23 | forceTerminationGracePeriod: 5s 24 | # ... 25 | ``` 26 | 27 | ## Flag 28 | 29 | ```bash 30 | chainsaw test --force-termination-grace-period 5s ... 31 | ``` 32 | -------------------------------------------------------------------------------- /testdata/e2e/examples/script-env/chainsaw-test.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Test 4 | metadata: 5 | name: script-env 6 | spec: 7 | bindings: 8 | - name: chainsaw 9 | value: chainsaw 10 | steps: 11 | - bindings: 12 | - name: hello 13 | value: hello 14 | try: 15 | - script: 16 | env: 17 | - name: GREETINGS 18 | value: (join(' ', [$hello, $chainsaw])) 19 | content: echo $GREETINGS 20 | check: 21 | ($error): ~ 22 | ($stdout): hello chainsaw 23 | - command: 24 | env: 25 | - name: GREETINGS 26 | value: (join(' ', [$hello, $chainsaw])) 27 | entrypoint: echo 28 | args: 29 | - $GREETINGS 30 | check: 31 | ($error): ~ 32 | ($stdout): hello chainsaw 33 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - asasalint 4 | - asciicheck 5 | - bidichk 6 | - bodyclose 7 | - containedctx 8 | - decorder 9 | - dogsled 10 | - durationcheck 11 | - errcheck 12 | - errname 13 | - exportloopref 14 | - gci 15 | - gochecknoinits 16 | - gofmt 17 | - gofumpt 18 | - goimports 19 | - goprintffuncname 20 | - gosec 21 | - gosimple 22 | - govet 23 | - grouper 24 | - importas 25 | - ineffassign 26 | - makezero 27 | - misspell 28 | - noctx 29 | - nolintlint 30 | - nosprintfhostport 31 | # - paralleltest 32 | - staticcheck 33 | - tenv 34 | - thelper 35 | - tparallel 36 | - typecheck 37 | - unconvert 38 | - unused 39 | - wastedassign 40 | - whitespace 41 | 42 | run: 43 | timeout: 15m 44 | skip-files: 45 | - ".+\\.generated.go" 46 | output: 47 | format: colored-line-number 48 | sort-results: true -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/events.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // Events defines how to collect events. 8 | type Events struct { 9 | // Timeout for the operation. Overrides the global timeout set in the Configuration. 10 | // +optional 11 | Timeout *metav1.Duration `json:"timeout,omitempty"` 12 | 13 | // Cluster defines the target cluster (default cluster will be used if not specified and/or overridden). 14 | // +optional 15 | Cluster string `json:"cluster,omitempty"` 16 | 17 | // Clusters holds a registry to clusters to support multi-cluster tests. 18 | // +optional 19 | Clusters map[string]Cluster `json:"clusters,omitempty"` 20 | 21 | // ObjectLabelsSelector determines the selection process of referenced objects. 22 | ObjectLabelsSelector `json:",inline"` 23 | 24 | // Format determines the output format (json or yaml). 25 | // +optional 26 | Format Format `json:"format,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /pkg/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/commands/assert" 5 | "github.com/kyverno/chainsaw/pkg/commands/build" 6 | "github.com/kyverno/chainsaw/pkg/commands/create" 7 | "github.com/kyverno/chainsaw/pkg/commands/docs" 8 | "github.com/kyverno/chainsaw/pkg/commands/export" 9 | "github.com/kyverno/chainsaw/pkg/commands/lint" 10 | "github.com/kyverno/chainsaw/pkg/commands/migrate" 11 | "github.com/kyverno/chainsaw/pkg/commands/root" 12 | "github.com/kyverno/chainsaw/pkg/commands/test" 13 | "github.com/kyverno/chainsaw/pkg/commands/version" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func RootCommand() *cobra.Command { 18 | cmd := root.Command() 19 | cmd.AddCommand( 20 | assert.Command(), 21 | build.Command(), 22 | create.Command(), 23 | docs.Command(), 24 | export.Command(), 25 | lint.Command(), 26 | migrate.Command(), 27 | test.Command(), 28 | version.Command(), 29 | ) 30 | return cmd 31 | } 32 | -------------------------------------------------------------------------------- /website/docs/commands/chainsaw.md: -------------------------------------------------------------------------------- 1 | ## chainsaw 2 | 3 | Stronger tool for e2e testing 4 | 5 | ``` 6 | chainsaw [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for chainsaw 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [chainsaw assert](chainsaw_assert.md) - Evaluate assertion 18 | * [chainsaw build](chainsaw_build.md) - Build commands 19 | * [chainsaw completion](chainsaw_completion.md) - Generate the autocompletion script for the specified shell 20 | * [chainsaw create](chainsaw_create.md) - Create Chainsaw resources 21 | * [chainsaw docs](chainsaw_docs.md) - Generate reference documentation 22 | * [chainsaw export](chainsaw_export.md) - Export commands 23 | * [chainsaw lint](chainsaw_lint.md) - Lint a file or read from standard input 24 | * [chainsaw migrate](chainsaw_migrate.md) - Migrate resources to Chainsaw 25 | * [chainsaw test](chainsaw_test.md) - Run tests 26 | * [chainsaw version](chainsaw_version.md) - Print the version informations 27 | 28 | -------------------------------------------------------------------------------- /.release-notes/v0.1.3.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Release notes for `v0.1.3`. 4 | 5 | ## 💫 New features 💫 6 | 7 | - Added support for running chainsaw without a Kubernetes cluster 8 | - Automatically add JSON schemas when converting tests from KUTTL to Chainsaw 9 | - Added support for glob patterns in operation `file` 10 | - Added support for passing arbitrary values when invoking `chainsaw test` 11 | 12 | ## 🔧 Fixes 🔧 13 | 14 | - Fixed a couple of KUTTL to chainsaw migration bugs 15 | - Fixed a bug where chainsaw doesn't throw an error when a wrong path is provided 16 | 17 | ## 📚 Docs 📚 18 | 19 | - Added community meeting docs 20 | - Added Google Group in community docs and README 21 | - Removed all references to `TestStep` in the docs as this is not supported anymore 22 | 23 | ## 🎸 Misc 🎸 24 | 25 | - Added Kubebuilder community recording link where chainsaw was presented 26 | - Added a `MAINTAINERS.md` file 27 | - Added CI job to check PR semantics 28 | - Added CI job to check test catalog 29 | -------------------------------------------------------------------------------- /pkg/validation/test/resource_reference.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 5 | "k8s.io/apimachinery/pkg/util/validation/field" 6 | ) 7 | 8 | func ValidateResourceReference(path *field.Path, obj v1alpha1.ResourceReference) field.ErrorList { 9 | var errs field.ErrorList 10 | if obj.Kind == "" && obj.Resource == "" { 11 | errs = append(errs, field.Invalid(path, obj, "kind or resource must be specified")) 12 | } else if obj.Kind != "" && obj.Resource != "" { 13 | errs = append(errs, field.Invalid(path, obj, "kind or resource must be specified (found both)")) 14 | } else if obj.Resource != "" && obj.APIVersion != "" { 15 | errs = append(errs, field.Invalid(path.Child("apiVersion"), obj, "apiVersion must not be specified when resource is set")) 16 | } else if obj.Kind != "" && obj.APIVersion == "" { 17 | errs = append(errs, field.Invalid(path.Child("apiVersion"), obj, "apiVersion must be specified when kind is set")) 18 | } 19 | return errs 20 | } 21 | -------------------------------------------------------------------------------- /pkg/runner/logging/section.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "slices" 7 | "strings" 8 | 9 | "go.uber.org/multierr" 10 | utilerrors "k8s.io/apimachinery/pkg/util/errors" 11 | ) 12 | 13 | type section struct { 14 | name string 15 | args []any 16 | } 17 | 18 | func (s section) String() string { 19 | return strings.TrimSpace(s.name + "\n" + fmt.Sprint(s.args...)) 20 | } 21 | 22 | func Section(name string, args ...any) fmt.Stringer { 23 | return section{ 24 | name: "=== " + strings.ToUpper(name), 25 | args: args, 26 | } 27 | } 28 | 29 | func ErrSection(err error) fmt.Stringer { 30 | var errs []string 31 | for _, err := range multierr.Errors(err) { 32 | var agg utilerrors.Aggregate 33 | if errors.As(err, &agg) { 34 | for _, err := range agg.Errors() { 35 | errs = append(errs, err.Error()) 36 | } 37 | } else { 38 | errs = append(errs, err.Error()) 39 | } 40 | } 41 | slices.Sort(errs) 42 | return Section("ERROR", strings.Join(errs, "\n")) 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Lint 4 | 5 | permissions: {} 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - main 11 | - release-* 12 | push: 13 | branches: 14 | - main 15 | - release-* 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | golangci-lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 27 | - name: Setup Go 28 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 29 | with: 30 | go-version-file: go.mod 31 | cache-dependency-path: go.sum 32 | - name: golangci-lint 33 | uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v3.7.1 34 | with: 35 | skip-cache: true 36 | -------------------------------------------------------------------------------- /pkg/utils/fs/check_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCheckFolders(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | paths []string 13 | wantErr bool 14 | }{{ 15 | name: "nil", 16 | paths: nil, 17 | wantErr: false, 18 | }, { 19 | name: "empty", 20 | paths: []string{}, 21 | wantErr: false, 22 | }, { 23 | name: "valid", 24 | paths: []string{ 25 | ".", 26 | "..", 27 | }, 28 | wantErr: false, 29 | }, { 30 | name: "invalid", 31 | paths: []string{ 32 | ".", 33 | "../foo", 34 | }, 35 | wantErr: true, 36 | }, { 37 | name: "invalid", 38 | paths: []string{ 39 | "../foo", 40 | }, 41 | wantErr: true, 42 | }} 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | err := CheckFolders(tt.paths...) 46 | if tt.wantErr { 47 | assert.Error(t, err) 48 | } else { 49 | assert.NoError(t, err) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /testdata/e2e/examples/bindings/README.md: -------------------------------------------------------------------------------- 1 | # Test: `bindings` 2 | 3 | *No description* 4 | 5 | ## Bindings 6 | 7 | | # | Name | Value | 8 | |:-:|---|---| 9 | | 1 | `a` | 1 | 10 | 11 | ## Steps 12 | 13 | | # | Name | Bindings | Try | Catch | Finally | 14 | |:-:|---|:-:|:-:|:-:|:-:| 15 | | 1 | [step-1](#step-step-1) | 1 | 1 | 0 | 0 | 16 | | 2 | [step-2](#step-step-2) | 1 | 1 | 0 | 0 | 17 | 18 | ### Step: `step-1` 19 | 20 | *No description* 21 | 22 | #### Bindings 23 | 24 | | # | Name | Value | 25 | |:-:|---|---| 26 | | 1 | `b` | 2 | 27 | 28 | #### Try 29 | 30 | | # | Operation | Bindings | Outputs | Description | 31 | |:-:|---|:-:|:-:|---| 32 | | 1 | `apply` | 1 | 0 | *No description* | 33 | 34 | ### Step: `step-2` 35 | 36 | *No description* 37 | 38 | #### Bindings 39 | 40 | | # | Name | Value | 41 | |:-:|---|---| 42 | | 1 | `b` | 2 | 43 | 44 | #### Try 45 | 46 | | # | Operation | Bindings | Outputs | Description | 47 | |:-:|---|:-:|:-:|---| 48 | | 1 | `assert` | 1 | 0 | *No description* | 49 | 50 | --- 51 | 52 | -------------------------------------------------------------------------------- /pkg/runner/mutate/merge.go: -------------------------------------------------------------------------------- 1 | package mutate 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmespath-community/go-jmespath/pkg/binding" 7 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 8 | "github.com/kyverno/chainsaw/pkg/mutate" 9 | "github.com/kyverno/chainsaw/pkg/runner/functions" 10 | mapsutils "github.com/kyverno/chainsaw/pkg/utils/maps" 11 | "github.com/kyverno/kyverno-json/pkg/engine/template" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | ) 14 | 15 | func Merge(ctx context.Context, obj unstructured.Unstructured, bindings binding.Bindings, modifiers ...v1alpha1.Any) (unstructured.Unstructured, error) { 16 | for _, modifier := range modifiers { 17 | patch, err := mutate.Mutate(ctx, nil, mutate.Parse(ctx, modifier.Value), obj.UnstructuredContent(), bindings, template.WithFunctionCaller(functions.Caller)) 18 | if err != nil { 19 | return obj, err 20 | } 21 | obj.SetUnstructuredContent(mapsutils.Merge(obj.UnstructuredContent(), convertMap(patch))) 22 | } 23 | return obj, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/validation/config/cluster_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | func TestValidateCluster(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | path *field.Path 15 | obj v1alpha1.Cluster 16 | want field.ErrorList 17 | }{{ 18 | name: "empty", 19 | path: field.NewPath("foo"), 20 | want: field.ErrorList{ 21 | &field.Error{ 22 | Type: field.ErrorTypeRequired, 23 | BadValue: "", 24 | Field: "foo.kubeconfig", 25 | Detail: "a kubeconfig is required", 26 | }, 27 | }, 28 | }, { 29 | name: "valid", 30 | path: field.NewPath("foo"), 31 | obj: v1alpha1.Cluster{ 32 | Kubeconfig: "foo", 33 | }, 34 | }} 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | got := ValidateCluster(tt.path, tt.obj) 38 | assert.Equal(t, tt.want, got) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/commands/lint/processor.go: -------------------------------------------------------------------------------- 1 | package lint 2 | 3 | import ( 4 | "fmt" 5 | 6 | yaml "k8s.io/apimachinery/pkg/util/yaml" 7 | ) 8 | 9 | type FileFormatProcessor interface { 10 | ToJSON(input []byte) ([]byte, error) 11 | } 12 | 13 | type ( 14 | JSONProcessor struct{} 15 | YAMLProcessor struct{} 16 | ) 17 | 18 | func (p JSONProcessor) ToJSON(input []byte) ([]byte, error) { 19 | return input, nil 20 | } 21 | 22 | func (p YAMLProcessor) ToJSON(input []byte) ([]byte, error) { 23 | return yaml.ToJSON(input) 24 | } 25 | 26 | func getProcessor(format string, input []byte) (FileFormatProcessor, error) { 27 | if format == "" { 28 | format = detectFormat(input) 29 | } 30 | switch format { 31 | case ".json": 32 | return JSONProcessor{}, nil 33 | case ".yaml": 34 | return YAMLProcessor{}, nil 35 | default: 36 | return nil, fmt.Errorf("unsupported file format: %s", format) 37 | } 38 | } 39 | 40 | func detectFormat(input []byte) string { 41 | if yaml.IsJSONBuffer(input) { 42 | return ".json" 43 | } 44 | return ".yaml" 45 | } 46 | -------------------------------------------------------------------------------- /pkg/runner/operations/sleep/operation.go: -------------------------------------------------------------------------------- 1 | package sleep 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/jmespath-community/go-jmespath/pkg/binding" 8 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 9 | "github.com/kyverno/chainsaw/pkg/runner/logging" 10 | "github.com/kyverno/chainsaw/pkg/runner/operations" 11 | "github.com/kyverno/chainsaw/pkg/runner/operations/internal" 12 | ) 13 | 14 | type operation struct { 15 | duration v1alpha1.Sleep 16 | } 17 | 18 | func New(duration v1alpha1.Sleep) operations.Operation { 19 | return &operation{ 20 | duration: duration, 21 | } 22 | } 23 | 24 | func (o *operation) Exec(ctx context.Context, _ binding.Bindings) (_ operations.Outputs, _err error) { 25 | logger := internal.GetLogger(ctx, nil) 26 | defer func() { 27 | internal.LogEnd(logger, logging.Sleep, _err) 28 | }() 29 | internal.LogStart(logger, logging.Sleep) 30 | return nil, o.execute() 31 | } 32 | 33 | func (o *operation) execute() error { 34 | time.Sleep(o.duration.Duration.Duration) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /website/apis/markdown/type.tpl: -------------------------------------------------------------------------------- 1 | {{- define "type" }} 2 | ## `{{ .Name.Name }}` {#{{ .Anchor }}} 3 | {{- if eq .Kind "Alias" }} 4 | 5 | (Alias of `{{ .Underlying }}`) 6 | {{- end }} 7 | {{- with .References }} 8 | 9 | **Appears in:** 10 | {{ range . }} 11 | {{- if or .Referenced .IsExported }} 12 | - [{{ .DisplayName }}]({{ .Link }}) 13 | {{- end }} 14 | {{- end }} 15 | {{- end }} 16 | {{- if .GetComment }} 17 | 18 | {{ .GetComment }} 19 | {{- end }} 20 | {{- if .GetMembers }} 21 | 22 | | Field | Type | Required | Inline | Description | 23 | |---|---|---|---|---| 24 | {{- /* . is a apiType */}} 25 | {{- if .IsExported }} 26 | {{- /* Add apiVersion and kind rows if deemed necessary */}} 27 | | `apiVersion` | `string` | :white_check_mark: | | `{{- .APIGroup -}}` | 28 | | `kind` | `string` | :white_check_mark: | | `{{- .Name.Name -}}` | 29 | {{- end }} 30 | {{- /* The actual list of members is in the following template */}} 31 | {{- template "members" . }} 32 | {{- end }} 33 | {{ end }} 34 | -------------------------------------------------------------------------------- /pkg/runner/namespacer/namespacer.go: -------------------------------------------------------------------------------- 1 | package namespacer 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kyverno/chainsaw/pkg/client" 7 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type Namespacer interface { 11 | Apply(ctrlclient.Object) error 12 | GetNamespace() string 13 | } 14 | 15 | type namespacer struct { 16 | c client.Client 17 | namespace string 18 | } 19 | 20 | func New(c client.Client, namespace string) Namespacer { 21 | return &namespacer{ 22 | c: c, 23 | namespace: namespace, 24 | } 25 | } 26 | 27 | func (n *namespacer) Apply(resource ctrlclient.Object) error { 28 | if resource == nil { 29 | return errors.New("resource is nil") 30 | } 31 | if resource.GetNamespace() == "" { 32 | namespaced, err := n.c.IsObjectNamespaced(resource) 33 | if err != nil { 34 | return err 35 | } 36 | if namespaced { 37 | resource.SetNamespace(n.namespace) 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (n *namespacer) GetNamespace() string { 44 | return n.namespace 45 | } 46 | -------------------------------------------------------------------------------- /pkg/runner/operations/internal/env.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/jmespath-community/go-jmespath/pkg/binding" 8 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 9 | apibindings "github.com/kyverno/chainsaw/pkg/runner/bindings" 10 | ) 11 | 12 | func RegisterEnvs(ctx context.Context, namespace string, bindings binding.Bindings, envs ...v1alpha1.Binding) (map[string]string, []string, error) { 13 | mapOut := map[string]string{} 14 | var envsOut []string 15 | for _, env := range envs { 16 | name, value, err := apibindings.ResolveBinding(ctx, bindings, nil, env) 17 | if err != nil { 18 | return mapOut, envsOut, err 19 | } 20 | if value, ok := value.(string); !ok { 21 | return mapOut, envsOut, fmt.Errorf("value must be a string (%s)", env.Name) 22 | } else { 23 | mapOut[name] = value 24 | envsOut = append(envsOut, name+"="+value) 25 | } 26 | } 27 | mapOut["NAMESPACE"] = namespace 28 | envsOut = append(envsOut, "NAMESPACE="+namespace) 29 | return mapOut, envsOut, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/validation/test/check_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | func TestValidateCheck(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | path *field.Path 15 | obj *v1alpha1.Check 16 | want field.ErrorList 17 | }{{ 18 | name: "nil", 19 | path: nil, 20 | obj: nil, 21 | want: nil, 22 | }, { 23 | name: "no value", 24 | path: field.NewPath("foo"), 25 | obj: &v1alpha1.Check{}, 26 | want: field.ErrorList{ 27 | field.Invalid(field.NewPath("foo"), &v1alpha1.Check{}, "a value must be specified"), 28 | }, 29 | }, { 30 | name: "valid", 31 | path: field.NewPath("foo"), 32 | obj: &v1alpha1.Check{ 33 | Value: 42, 34 | }, 35 | want: nil, 36 | }} 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | got := ValidateCheck(tt.path, tt.obj) 40 | assert.Equal(t, tt.want, got) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /website/docs/configuration/values.md: -------------------------------------------------------------------------------- 1 | # Passing data to tests 2 | 3 | Chainsaw can pass arbitrary values when running tests using the `--values` flag. 4 | Values will be available to tests under the `$values` binding. 5 | 6 | This is useful when a test needs to be configured externally. 7 | 8 | ## Example 9 | 10 | The test below expects the `$value.foo` to be provided when chainsaw is invoked. 11 | 12 | ```yaml 13 | apiVersion: chainsaw.kyverno.io/v1alpha1 14 | kind: Test 15 | metadata: 16 | name: values 17 | spec: 18 | steps: 19 | - try: 20 | - assert: 21 | resource: 22 | ($values.foo): bar 23 | ``` 24 | 25 | Now you can invoke chainsaw like this: 26 | 27 | ```bash 28 | # pass object { "foo": "bar" } as values to the executed tests 29 | # `--values -` means values are read from standard input 30 | echo "foo: bar" | chainsaw test --values - 31 | 32 | # read values from a file 33 | chainsaw test --values ./values.yaml 34 | 35 | # pass values using heredoc 36 | chainsaw test --values - <