Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions e2etests/bats-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ get_value_from() {
[[ "${message2}" =~ "Pod: resource is not valid:" ]]
}

@test "builtin-schema-validation" {
tmp="tests/checks/kubeconform.yml"
cmd="${KUBE_LINTER_BIN} lint --config e2etests/testdata/schema-validation-config.yaml --do-not-auto-add-defaults --format json ${tmp}"
run ${cmd}

print_info "${status}" "${output}" "${cmd}" "${tmp}"
[ "$status" -eq 1 ]

message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message')
message2=$(get_value_from "${lines[0]}" '.Reports[1].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[1].Diagnostic.Message')
count=$(get_value_from "${lines[0]}" '.Reports | length')

# Should find 2 validation errors using builtin schema-validation check
[[ "${count}" == "2" ]]
[[ "${message1}" =~ "DaemonSet: resource is not valid:" ]]
[[ "${message2}" =~ "Pod: resource is not valid:" ]]
}

@test "template-check-installed-bash-version" {
run "bash --version"
[[ "${BASH_VERSION:0:1}" -ge '4' ]] || false
Expand Down
1 change: 1 addition & 0 deletions pkg/command/lint/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func Command() *cobra.Command {
c.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
c.Flags().Var(format, "format", format.Usage())
c.Flags().BoolVarP(&errorOnInvalidResource, "fail-on-invalid-resource", "", false, "Error out when we have an invalid resource")
_ = c.Flags().MarkDeprecated("fail-on-invalid-resource", "Use 'schema-validation' builtin check or kubeconform template for better schema validation.")

config.AddFlags(c, v)
return c
Expand Down
21 changes: 2 additions & 19 deletions pkg/command/lint/testdata/invalid-pod-resources.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
apiVersion: v1
kind: Pod
kind: InvalidKind
metadata:
creationTimestamp: null
name: foo-pod
namespace: foo
spec:
containers:
- image: busybox
name: invalid
command:
- "sleep"
args:
- "infinity"
resources:
limits:
cpu: 25m
memory: 1GB
requests:
cpu: 25m
memory: 1GB
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
invalidField: [this is invalid YAML that should fail to parse
14 changes: 2 additions & 12 deletions pkg/command/lint/testdata/invalid-pvc-resources.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: foo-pvc
namespace: foo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 250GB
storageClassName: thin-disk
this is malformed YAML that should fail to parse: {
invalid: unclosed bracket
6 changes: 6 additions & 0 deletions pkg/lintcontext/create_contexts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ func TestCreateContextsWithIgnorePaths(t *testing.T) {
"../../.pre-commit-hooks*",
"../../dist/**/*",
"../../pkg/**/*",
"../../demo/**",
"../../stackrox-kube-linter-bug-example/**",
"../../tests/**/*",
"../../cmd/**/*",
"../../docs/**/*",
"../../internal/**/*",
"/**/*/checks/**/*",
"/**/*/test_helper/**/*",
"/**/*/testdata/**/*",
Expand Down
14 changes: 13 additions & 1 deletion pkg/lintcontext/parse_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
"helm.sh/helm/v3/pkg/engine"
autoscalingV2Beta1 "k8s.io/api/autoscaling/v2beta1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
runtimeYaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
y "sigs.k8s.io/yaml"
Expand Down Expand Up @@ -58,7 +60,17 @@ func parseObjects(data []byte, d runtime.Decoder) ([]k8sutil.Object, error) {
}
obj, _, err := d.Decode(data, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to decode: %w", err)
// this is for backward compatibility, should be replaced with kubeconform
if strings.Contains(err.Error(), "json: cannot unmarshal") {
return nil, fmt.Errorf("failed to decode: %w", err)
}
// fallback to unstructured as schema validation will be performed by kubeconform check
dec := runtimeYaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
var unstructuredErr error
obj, _, unstructuredErr = dec.Decode(data, nil, obj)
if unstructuredErr != nil {
return nil, fmt.Errorf("failed to decode: %w: %w", err, unstructuredErr)
}
}
if list, ok := obj.(*v1.List); ok {
objs := make([]k8sutil.Object, 0, len(list.Items))
Expand Down
263 changes: 263 additions & 0 deletions pkg/lintcontext/parse_yaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package lintcontext

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
)

func TestParseObjects(t *testing.T) {
tests := []struct {
name string
yamlData string
expectError bool
expectCount int
expectKind string
expectName string
}{
{
name: "valid Pod",
yamlData: `apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: default
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80`,
expectError: false,
expectCount: 1,
expectKind: "Pod",
expectName: "test-pod",
},
{
name: "valid Service",
yamlData: `apiVersion: v1
kind: Service
metadata:
name: test-service
namespace: default
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: ClusterIP`,
expectError: false,
expectCount: 1,
expectKind: "Service",
expectName: "test-service",
},
{
name: "Tekton Task CRD",
yamlData: `apiVersion: tekton.dev/v1
kind: Task
metadata:
name: hello-world-task
namespace: default
spec:
description: A simple hello world task
steps:
- name: hello
image: alpine:latest
command:
- echo
args:
- "Hello World!"`,
expectError: false,
expectCount: 1,
expectKind: "Task",
expectName: "hello-world-task",
},
{
name: "List with multiple objects",
yamlData: `apiVersion: v1
kind: List
metadata: {}
items:
- apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
containers:
- name: nginx
image: nginx:latest
- apiVersion: v1
kind: Service
metadata:
name: service1
spec:
selector:
app: nginx
ports:
- port: 80`,
expectError: false,
expectCount: 2,
expectKind: "Pod", // First object
expectName: "pod1",
},
{
name: "invalid YAML",
yamlData: `apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
invalidField: this-should-not-be-here
containers:
- name: nginx
image: nginx:latest
invalidContainerField: also-invalid`,
expectError: false, // parseObjects doesn't validate schema, only structure
expectCount: 1,
expectKind: "Pod",
expectName: "test-pod",
},
{
name: "malformed YAML",
yamlData: `apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: "invalid-port-type"`, // string instead of int
expectError: true, // Should fail due to type mismatch
expectCount: 0,
expectKind: "",
expectName: "",
},
{
name: "unknown Kubernetes resource type",
yamlData: `apiVersion: example.com/v1
kind: CustomResource
metadata:
name: test-custom
namespace: default
spec:
customField: value`,
expectError: false,
expectCount: 1,
expectKind: "CustomResource",
expectName: "test-custom",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objects, err := parseObjects([]byte(tt.yamlData), nil)

if tt.expectError {
assert.Error(t, err, "Expected parseObjects to return an error")
assert.Len(t, objects, tt.expectCount)
} else {
assert.NoError(t, err, "Expected parseObjects to succeed")
require.Len(t, objects, tt.expectCount, "Expected specific number of objects")

if tt.expectCount > 0 {
// Check first object
firstObj := objects[0]
assert.Equal(t, tt.expectKind, firstObj.GetObjectKind().GroupVersionKind().Kind)
assert.Equal(t, tt.expectName, firstObj.GetName())

// Additional validation for Pod objects
if tt.expectKind == "Pod" {
pod, ok := firstObj.(*corev1.Pod)
require.True(t, ok, "Expected object to be a Pod")
assert.Equal(t, "v1", pod.APIVersion)
assert.Equal(t, "Pod", pod.Kind)
assert.NotEmpty(t, pod.Spec.Containers, "Expected Pod to have containers")
}
}
}
})
}
}

func TestParseObjectsWithCustomDecoder(t *testing.T) {
// Test that parseObjects can handle CRDs by falling back to unstructured parsing
tektonTaskYAML := `apiVersion: tekton.dev/v1
kind: Task
metadata:
name: hello-world-task
spec:
description: A simple hello world task
steps:
- name: hello
image: alpine:latest
command:
- echo
args:
- "Hello World!"`

// Test with default decoder (should succeed by falling back to unstructured)
objects, err := parseObjects([]byte(tektonTaskYAML), nil)
assert.NoError(t, err, "Expected Tekton Task to parse as unstructured with default decoder")
assert.Len(t, objects, 1)
assert.Equal(t, "Task", objects[0].GetObjectKind().GroupVersionKind().Kind)
assert.Equal(t, "hello-world-task", objects[0].GetName())

// Test with explicit decoder (should also succeed)
objects, err = parseObjects([]byte(tektonTaskYAML), decoder)
assert.NoError(t, err, "Expected Tekton Task to parse as unstructured with explicit decoder")
assert.Len(t, objects, 1)
assert.Equal(t, "Task", objects[0].GetObjectKind().GroupVersionKind().Kind)
assert.Equal(t, "hello-world-task", objects[0].GetName())
}

func TestParseObjectsEmptyInput(t *testing.T) {
// Test empty input
objects, err := parseObjects([]byte(""), nil)
assert.Error(t, err, "Expected empty input to return an error")
assert.Empty(t, objects)

// Test whitespace only
objects, err = parseObjects([]byte(" \n \t \n"), nil)
assert.Error(t, err, "Expected whitespace-only input to return an error")
assert.Empty(t, objects)
}

func TestParseObjectsValidateObjectInterface(t *testing.T) {
// Test that parsed objects implement the k8sutil.Object interface correctly
podYAML := `apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: test-namespace
labels:
app: test
annotations:
test: annotation
spec:
containers:
- name: nginx
image: nginx:latest`

objects, err := parseObjects([]byte(podYAML), nil)
require.NoError(t, err)
require.Len(t, objects, 1)

pod := objects[0]

// Test Object interface methods
assert.Equal(t, "test-pod", pod.GetName())
assert.Equal(t, "test-namespace", pod.GetNamespace())
assert.Equal(t, map[string]string{"app": "test"}, pod.GetLabels())
assert.Equal(t, map[string]string{"test": "annotation"}, pod.GetAnnotations())

// Test GroupVersionKind
gvk := pod.GetObjectKind().GroupVersionKind()
assert.Empty(t, gvk.Group)
assert.Equal(t, "v1", gvk.Version)
assert.Equal(t, "Pod", gvk.Kind)
}
Loading
Loading