Skip to content

Commit

Permalink
Add crd test command and GitHub action config (#96)
Browse files Browse the repository at this point in the history
* feat: add crd testing capability

Signed-off-by: Gergely Brautigam <[email protected]>

* updated the validator and updated the string matcher

Signed-off-by: Gergely Brautigam <[email protected]>

* using path now and checking the right versions for the right snapshots

* do not clutter the output with the general CLI help output on failure

* added skipping random generation and fixed additional properties parsing

* adding readme, usage and release document

---------

Signed-off-by: Gergely Brautigam <[email protected]>
  • Loading branch information
Skarlso authored Aug 19, 2024
1 parent 17693ec commit 858e82c
Show file tree
Hide file tree
Showing 38 changed files with 3,484 additions and 52 deletions.
4 changes: 2 additions & 2 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ linters-settings:
lines: 110
statements: 60
cyclop:
max-complexity: 46
max-complexity: 50
skip-tests: true
gocognit:
# Minimal code complexity to report.
# Default: 30 (but we recommend 10-20)
min-complexity: 46
min-complexity: 50
nolintlint:
allow-unused: false
require-explanation: true
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

Generate a sample YAML file from a CRD definition.

## CRD Testing using CTY

For more information about how to use `cty` for helm-like unit testing your CRD schemas,
please follow the [How to test CRDs with CTY Readme](./crd-testing-README.md).

![crd-unittest-sample-output](./imgs/crd-unittest-outcome.png)

## Getting started
- Prerequisites: Go installed on your machine. (Check out this link for details: https://go.dev/doc/install)
- Clone the repository
Expand Down
4 changes: 3 additions & 1 deletion cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type rootArgs struct {
stdOut bool
comments bool
minimal bool
skipRandom bool
}

var (
Expand Down Expand Up @@ -62,6 +63,7 @@ func init() {
f.BoolVarP(&args.stdOut, "stdout", "s", false, "If set, it will output the generated content to stdout.")
f.BoolVarP(&args.comments, "comments", "m", false, "If set, it will add descriptions as comments to each line where available.")
f.BoolVarP(&args.minimal, "minimal", "l", false, "If set, only the minimal required example yaml is generated.")
f.BoolVar(&args.skipRandom, "no-random", false, "Skip generating random values that satisfy the property patterns.")
}

func runGenerate(_ *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -116,7 +118,7 @@ func runGenerate(_ *cobra.Command, _ []string) error {
continue
}

errs = append(errs, pkg.Generate(crd, w, args.comments, args.minimal))
errs = append(errs, pkg.Generate(crd, w, args.comments, args.minimal, args.skipRandom))
}

return errors.Join(errs...)
Expand Down
91 changes: 91 additions & 0 deletions cmd/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cmd

import (
"fmt"
"os"

"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/spf13/cobra"

"github.com/Skarlso/crd-to-sample-yaml/pkg/tests"
)

const wrapLen = 80

var (
// testCmd is root for various `test ...` commands.
testCmd = &cobra.Command{
Use: "test",
Short: "Run a set of tests to check CRD schema stability.",
Run: runTest,
}

testArgs struct {
update bool
}
)

func init() {
rootCmd.AddCommand(testCmd)

f := testCmd.PersistentFlags()
f.BoolVarP(&testArgs.update, "update", "u", false, "Update any existing snapshots.")
}

func runTest(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "test needs an argument where the tests are located at")

os.Exit(1)
}

path := args[0]
runner := tests.NewSuiteRunner(path, testArgs.update)
outcome, err := runner.Run(cmd.Context())
if err != nil {
os.Exit(1)
}

if err := displayWarnings(outcome); err != nil {
os.Exit(1)
}
}

func displayWarnings(warnings []tests.Outcome) error {
errs := 0

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Status", "It", "Matcher", "Error", "Template"})
rows := make([]table.Row, 0, len(warnings))
for _, w := range warnings {
if w.Error != nil {
errs++
}

status := color.GreenString(w.Status)
if w.Status == "FAIL" {
status = color.RedString(w.Status)
}
var errText string
if w.Error != nil {
errText = text.WrapText(w.Error.Error(), wrapLen)
}
rows = append(rows, table.Row{
status, w.Name, w.Matcher, errText, w.Template,
})
}
t.AppendRows(rows)
t.AppendSeparator()
t.Render()

fmt.Fprintf(os.Stdout, "\nTests total: %d, failed: %d, passed: %d\n", len(warnings), errs, len(warnings)-errs)

if errs > 0 {
return fmt.Errorf("%d test(s) failed", errs)
}

return nil
}
141 changes: 141 additions & 0 deletions crd-testing-README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# CRD Testing with CTY

From version `v0.8.0` cty supports the command `test`.

`test` supports testing CRD schemas against snapshots of generated YAML files that satisfy the schema
or small snippets of yaml strings.

Adding this test to your CRDs makes sure that any modification on the CRD will not break a generated snapshot of the
CRD. This is from version to version, meaning the tests will make sure that API version is respected.

## Example

Let's look at an example.

Consider the following test suite definition that loosely follows the syntax of helm unittest definitions:

```yaml
suite: test crd bootstrap
template: crd-bootstrap/crds/bootstrap_crd.yaml # should point to a CRD that is regularly updated like in a helm chart.
tests:
- it: matches bootstrap crds correctly
asserts:
- matchSnapshot:
# this will generate one snapshot / CRD version and match all of them to the right version of the CRD
path: sample-tests/__snapshots__
- matchSnapshot:
path: sample-tests/__snapshots__
# generates a yaml file
minimal: true
- it: matches some custom stuff
asserts:
- matchString:
apiVersion: v1alpha1 # this will match this exact version only from the list of versions in the CRD
kind: Bootstrap
spec:
source:
url:
url: https://github.com/Skarlso/test
```
Put this into a file called `bootstrap_test.yaml`.

**IMPORTANT**: `test` will only consider yaml files that end with `_test.yaml`.

One test is per CRD file. A single CRD file, however, can contain multiple apiVersions. Therefore, it's important to
only target a specific version with a snapshot, otherwise, we might be testing something that is broken intentionally.

Now, we can run test like this:

```
./bin/cty test sample-tests
```

The locations in the suite are relative to the execution location.

Running test like this, will match the following snapshots with the given CRD assuming we have two version v1alpha1 and
v1beta1:
- bootstrap_crd-v1alpha1.yaml
- bootstrap_crd-v1alpha1.min.yaml
- bootstrap_crd-v1beta1.yaml
- bootstrap_crd-v1beta1.min.yaml

**_Note_**: At this release version the minimal version needs to be adjusted because it will generate an empty object without the closing `{}`.

If everything is okay, it will generate an output like this:

```
./bin/cty test sample-tests
+--------+----------------------------------+---------------+-------+--------------------------------------+
| STATUS | IT | MATCHER | ERROR | TEMPLATE |
+--------+----------------------------------+---------------+-------+--------------------------------------+
| PASS | matches bootstrap crds correctly | matchSnapshot | | sample-tests/crds/bootstrap_crd.yaml |
| PASS | matches bootstrap crds correctly | matchSnapshot | | sample-tests/crds/bootstrap_crd.yaml |
| PASS | matches some custom stuff | matchString | | sample-tests/crds/bootstrap_crd.yaml |
+--------+----------------------------------+---------------+-------+--------------------------------------+
Tests total: 3, failed: 0, passed: 3
```

If there _was_ an error, it should look something like this:

```
./bin/cty test sample-tests
+--------+----------------------------------+---------------+----------------------------------------------------------------------------------+--------------------------------------+
| STATUS | IT | MATCHER | ERROR | TEMPLATE |
+--------+----------------------------------+---------------+----------------------------------------------------------------------------------+--------------------------------------+
| PASS | matches bootstrap crds correctly | matchSnapshot | | sample-tests/crds/bootstrap_crd.yaml |
| PASS | matches bootstrap crds correctly | matchSnapshot | | sample-tests/crds/bootstrap_crd.yaml |
| FAIL | matches some custom stuff | matchString | matcher returned failure: failed to validate kind Bootstrap and version v1alpha1 | sample-tests/crds/bootstrap_crd.yaml |
| | | | : spec.source.url in body must be of type object: "null" | |
+--------+----------------------------------+---------------+----------------------------------------------------------------------------------+--------------------------------------+
Tests total: 3, failed: 1, passed: 2
```

In the above failing example, we forgot to define the URL field for source. Similarly, if the regex changes for a field
we should error and be alerted that it's a breaking change for any existing users.

```
./bin/cty test sample-tests
+--------+-----------------------------------+---------------+----------------------------------------------------------------------------------+--------------------------------------------------------------------+
| STATUS | IT | MATCHER | ERROR | TEMPLATE |
+--------+-----------------------------------+---------------+----------------------------------------------------------------------------------+--------------------------------------------------------------------+
| PASS | matches AWSCluster crds correctly | matchSnapshot | | sample-tests/crds/infrastructure.cluster.x-k8s.io_awsclusters.yaml |
| PASS | matches AWSCluster crds correctly | matchSnapshot | | sample-tests/crds/infrastructure.cluster.x-k8s.io_awsclusters.yaml |
| PASS | matches AWSCluster crds correctly | matchString | | sample-tests/crds/infrastructure.cluster.x-k8s.io_awsclusters.yaml |
| FAIL | matches AWSCluster crds correctly | matchString | matcher returned failure: failed to validate kind AWSCluster and version v1beta2 | sample-tests/crds/infrastructure.cluster.x-k8s.io_awsclusters.yaml |
| | | | : spec.s3Bucket.name in body should match '^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$' | |
| PASS | matches bootstrap crds correctly | matchSnapshot | | sample-tests/crds/bootstrap_crd.yaml |
| PASS | matches bootstrap crds correctly | matchSnapshot | | sample-tests/crds/bootstrap_crd.yaml |
| PASS | matches some custom stuff | matchString | | sample-tests/crds/bootstrap_crd.yaml |
+--------+-----------------------------------+---------------+----------------------------------------------------------------------------------+--------------------------------------------------------------------+

Tests total: 7, failed: 1, passed: 6
```
## Updating Snapshots
In order to generate snapshots for CRDs, simply add `--update` to the command:
```
./bin/cty test sample-tests --update
```
It should generate all snapshots and overwrite existing snapshots under the specified folder of the snapshot matcher.
Meaning consider the following yaml snippet from the above test:
```yaml
asserts:
- matchSnapshot:
# this will generate one snapshot / CRD version and match all of them to the right version of the CRD
path: sample-tests/__snapshots__
```

Provided this `path` the generated snapshots would end up under `sample-tests/__snapshots__` folder with a generated
name that will match the `template` field in the suite. If `template` field is changed, regenerate the tests and
delete any outdated snapshots.

## Examples

For further examples, please see under [sample-tests](./sample-tests).
27 changes: 27 additions & 0 deletions docs/release_notes/v0.8.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# v0.8.0

## MAJOR UPDATE

### Changes to how values are generated

This update contains a few modifications to the way we generate samples. These modifications are the following:

- if enum values are defined for a property, choose the first one from the list whatever that is
- if there is a minimum defined for integer types, the minimum value is used
- comment is added to list items of what type they are and how much the minimum value for them is
```yaml
volumeIDs: [] # minItems 0 of type string
```
- unless `no-random` is defined, now given a `Pattern` that contains a valid regex a valid value is generated that satisfies the regex
and the regex's value is commented after the value
```yaml
name: xwjhylgy2ruc # ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$
```

The random generation can be skipped by providing the following flag to `cty`: `--no-random`.

### New `test` command

A new command has been added that lets users unit test schema validation for generated YAML files to CRDs.

To read more about it, check out the readme: `crd-testing-README.md`.
36 changes: 34 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module github.com/Skarlso/crd-to-sample-yaml
go 1.23

require (
github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/fatih/color v1.17.0
github.com/jedib0t/go-pretty/v6 v6.5.9
github.com/maxence-charriere/go-app/v10 v10.0.5
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
Expand All @@ -11,27 +14,56 @@ require (
)

require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiserver v0.31.0 // indirect
k8s.io/component-base v0.31.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
Expand Down
Loading

0 comments on commit 858e82c

Please sign in to comment.