From 4eb75860d9223ea14b5b21bf5506ca76bc9197a1 Mon Sep 17 00:00:00 2001 From: Yann Hamon Date: Sat, 3 Jul 2021 13:26:45 +0200 Subject: [PATCH] support for lists --- acceptance.bats | 38 ++++++++++++++++++++++ fixtures/list_empty_valid.yaml | 4 +++ fixtures/multi_with_list.yaml | 26 +++++++++++++++ go.sum | 37 +++------------------- pkg/resource/files.go | 7 ++-- pkg/resource/resource.go | 58 ++++++++++++++++++++++++++-------- pkg/resource/resource_test.go | 36 +++++++++++++++++++++ pkg/resource/stream.go | 5 ++- 8 files changed, 162 insertions(+), 49 deletions(-) create mode 100644 fixtures/list_empty_valid.yaml create mode 100644 fixtures/multi_with_list.yaml diff --git a/acceptance.bats b/acceptance.bats index 78cf792..2f963b0 100755 --- a/acceptance.bats +++ b/acceptance.bats @@ -243,3 +243,41 @@ resetCacheFolder() { [ "${lines[1]}" == 'ok 1 - fixtures/valid.yaml (ReplicationController)' ] [ "${lines[2]}" == '1..1' ] } + +@test "Pass when parsing a file containing multiple a List" { + run bin/kubeconform -summary fixtures/list_valid.yaml + [ "$status" -eq 0 ] + [ "$output" = "Summary: 6 resources found in 1 file - Valid: 6, Invalid: 0, Errors: 0, Skipped: 0" ] +} + +@test "Pass when parsing a List resource from stdin" { + run bash -c "cat fixtures/list_valid.yaml | bin/kubeconform -summary" + [ "$status" -eq 0 ] + [ "$output" = 'Summary: 6 resources found parsing stdin - Valid: 6, Invalid: 0, Errors: 0, Skipped: 0' ] +} + +@test "Fail when parsing a List that contains an invalid resource" { + run bin/kubeconform -summary fixtures/list_invalid.yaml + [ "$status" -eq 1 ] + [ "${lines[0]}" == 'fixtures/list_invalid.yaml - ReplicationController bob is invalid: For field spec.replicas: Invalid type. Expected: [integer,null], given: string' ] + [ "${lines[1]}" == 'Summary: 2 resources found in 1 file - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0' ] +} + +@test "Fail when parsing a List that contains an invalid resource from stdin" { + run bash -c "cat fixtures/list_invalid.yaml | bin/kubeconform -summary -" + [ "$status" -eq 1 ] + [ "${lines[0]}" == 'stdin - ReplicationController bob is invalid: For field spec.replicas: Invalid type. Expected: [integer,null], given: string' ] + [ "${lines[1]}" == 'Summary: 2 resources found parsing stdin - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0' ] +} + +@test "Pass on valid, empty list" { + run bin/kubeconform -summary fixtures/list_empty_valid.yaml + [ "$status" -eq 0 ] + [ "$output" = 'Summary: 0 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0, Skipped: 0' ] +} + +@test "Pass on multi-yaml containing one resource, one list" { + run bin/kubeconform -summary fixtures/multi_with_list.yaml + [ "$status" -eq 0 ] + [ "$output" = 'Summary: 2 resources found in 1 file - Valid: 2, Invalid: 0, Errors: 0, Skipped: 0' ] +} diff --git a/fixtures/list_empty_valid.yaml b/fixtures/list_empty_valid.yaml new file mode 100644 index 0000000..2276fbb --- /dev/null +++ b/fixtures/list_empty_valid.yaml @@ -0,0 +1,4 @@ +--- +apiVersion: v1 +kind: List +items: [] \ No newline at end of file diff --git a/fixtures/multi_with_list.yaml b/fixtures/multi_with_list.yaml new file mode 100644 index 0000000..df8f379 --- /dev/null +++ b/fixtures/multi_with_list.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-master + labels: + app: redis + tier: backend + role: master +spec: + ports: + # the port that this service should serve on + - port: 6379 + targetPort: 6379 + selector: + app: redis + tier: backend + role: master +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Namespace + metadata: + name: b \ No newline at end of file diff --git a/go.sum b/go.sum index 905671a..f65ddfe 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,23 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357 h1:Rem2+U35z1QtPQc6r+WolF7yXiefXqDKyk+lN2pE164= -github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0 h1:j30noezaCfvNLcdMYSvHLv81DxYRSt1grlpseG67vhU= -github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/instrumenta/kubeval v0.0.0-20200515185822-7721cbec724c h1:tF3B96upB2wECZMXZxrAMLiVUgT22sNNxhuOhrcg28s= -github.com/instrumenta/kubeval v0.0.0-20200515185822-7721cbec724c/go.mod h1:cD+P/oZrBwOnaIHXrqvKPuN353KPxGomnsXSXf8pFJs= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= -github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/pelletier/go-toml v0.0.0-20180724185102-c2dbbc24a979/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550 h1:LB9SHuuXO8gnsHtexOQSpsJrrAHYA35lvHUaE74kznU= -github.com/spf13/cobra v0.0.0-20180820174524-ff0d02e85550/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086 h1:iU+nPfqRqK8ShQqnpZLv8cZ9oklo6NFUcmX1JT5Rudg= -github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -golang.org/x/sys v0.0.0-20180821044426-4ea2f632f6e9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.0.0-20180810153555-6e3c4e7365dd/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/resource/files.go b/pkg/resource/files.go index ce4bf01..c232509 100644 --- a/pkg/resource/files.go +++ b/pkg/resource/files.go @@ -94,8 +94,11 @@ func findResourcesInReader(p string, f io.Reader, resources chan<- Resource, err nRes := 0 for scanner.Scan() { if len(scanner.Text()) > 0 { - resources <- Resource{Path: p, Bytes: []byte(scanner.Text())} - nRes++ + res := Resource{Path: p, Bytes: []byte(scanner.Text())} + for _, subres := range res.Resources() { + resources <- subres + nRes++ + } } } if err := scanner.Err(); err != nil { diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 1d32c60..e4e8d30 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -2,15 +2,17 @@ package resource import ( "fmt" + "strings" "sigs.k8s.io/yaml" ) // Resource represents a Kubernetes resource within a file type Resource struct { - Path string - Bytes []byte - sig *Signature + Path string + Bytes []byte + sig *Signature // Cache signature parsing + sigErr error // Cache potential signature parsing error } // Signature is a key representing a Kubernetes resource @@ -21,7 +23,7 @@ type Signature struct { // Signature computes a signature for a resource, based on its Kind, Version, Namespace & Name func (res *Resource) Signature() (*Signature, error) { if res.sig != nil { - return res.sig, nil + return res.sig, res.sigErr } resource := struct { @@ -44,33 +46,38 @@ func (res *Resource) Signature() (*Signature, error) { res.sig = &Signature{Kind: resource.Kind, Version: resource.APIVersion, Namespace: resource.Metadata.Namespace, Name: name} if err != nil { // Exit if there was an error unmarshalling - return res.sig, err + res.sigErr = err + return res.sig, res.sigErr } - if resource.Kind == "" { - return res.sig, fmt.Errorf("missing 'kind' key") + if res.sig.Kind == "" { + res.sigErr = fmt.Errorf("missing 'kind' key") + return res.sig, res.sigErr } - if resource.APIVersion == "" { - return res.sig, fmt.Errorf("missing 'apiVersion' key") + if res.sig.Version == "" { + res.sigErr = fmt.Errorf("missing 'apiVersion' key") + return res.sig, res.sigErr } - return res.sig, err + return res.sig, res.sigErr } func (res *Resource) SignatureFromMap(m map[string]interface{}) (*Signature, error) { if res.sig != nil { - return res.sig, nil + return res.sig, res.sigErr } Kind, ok := m["kind"].(string) if !ok { - return res.sig, fmt.Errorf("missing 'kind' key") + res.sigErr = fmt.Errorf("missing 'kind' key") + return res.sig, res.sigErr } APIVersion, ok := m["apiVersion"].(string) if !ok { - return res.sig, fmt.Errorf("missing 'apiVersion' key") + res.sigErr = fmt.Errorf("missing 'apiVersion' key") + return res.sig, res.sigErr } var name, ns string @@ -87,3 +94,28 @@ func (res *Resource) SignatureFromMap(m map[string]interface{}) (*Signature, err res.sig = &Signature{Kind: Kind, Version: APIVersion, Namespace: ns, Name: name} return res.sig, nil } + +// Resources returns a list of resources if the resource is of type List, a single resource otherwise +// See https://github.com/yannh/kubeconform/issues/53 +func (res *Resource) Resources() []Resource { + resources := []Resource{} + if s, err := res.Signature(); err == nil && strings.ToLower(s.Kind) == "list" { + // A single file of type List + list := struct { + Version string + Kind string + Items []interface{} + }{} + + yaml.Unmarshal(res.Bytes, &list) + + for _, item := range list.Items { + r := Resource{Path: res.Path} + r.Bytes, _ = yaml.Marshal(item) + resources = append(resources, r) + } + return resources + } + + return []Resource{*res} +} diff --git a/pkg/resource/resource_test.go b/pkg/resource/resource_test.go index 42fbec3..48ce4d1 100644 --- a/pkg/resource/resource_test.go +++ b/pkg/resource/resource_test.go @@ -86,3 +86,39 @@ func TestSignatureFromMap(t *testing.T) { } } } + +func TestResources(t *testing.T) { + testCases := []struct { + b string + expected int + }{ + { + "apiVersion: v1\nkind: List\n", + 0, + }, + { + "apiVersion: v1\nkind: List\nItems: []\n", + 0, + }, + { + "apiVersion: v1\nkind: List\nItems:\n - apiVersion: v1\n kind: ReplicationController\n metadata:\n name: \"bob\"\n spec:\n replicas: 2\n", + 1, + }, + { + "apiVersion: v1\nkind: List\nItems:\n - apiVersion: v1\n kind: ReplicationController\n metadata:\n name: \"bob\"\n spec:\n replicas: 2\n - apiVersion: v1\n kind: ReplicationController\n metadata:\n name: \"Jim\"\n spec:\n replicas: 2\n", + 2, + }, + } + + for i, testCase := range testCases { + res := resource.Resource{ + Path: "foo", + Bytes: []byte(testCase.b), + } + + subres := res.Resources() + if len(subres) != testCase.expected { + t.Errorf("test %d: expected to find %d resources, found %d", i, testCase.expected, len(subres)) + } + } +} diff --git a/pkg/resource/stream.go b/pkg/resource/stream.go index 6ed328b..5cfbeb1 100644 --- a/pkg/resource/stream.go +++ b/pkg/resource/stream.go @@ -60,7 +60,10 @@ func FromStream(ctx context.Context, path string, r io.Reader) (<-chan Resource, break SCAN default: } - resources <- Resource{Path: path, Bytes: scanner.Bytes()} + res := Resource{Path: path, Bytes: scanner.Bytes()} + for _, subres := range res.Resources() { + resources <- subres + } } close(resources)