Skip to content

Commit 5c1ed24

Browse files
committed
Add fallback decoder for unknown resources to handle CRDs
This PR adds a fallback when decoding unknown resources to be able to handle CRDs. The schema validation could be then performed with kubeconform check from #1033 and CEL from #1012. This should fix #606 Changes: - Modified parseObjects to use unstructured decoder as fallback for unknown resource types - Added comprehensive test suite covering standard K8s resources and CRDs - Maintained backward compatibility for existing decode error handling - Added test cases for Tekton Task CRD and other custom resources The fallback allows kube-linter to parse CRDs like Tekton Pipelines without failing, while delegating proper schema validation to specialized templates like kubeconform and CEL expressions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Tomasz Janiszewski <tomek@redhat.com>
1 parent 49b9c87 commit 5c1ed24

File tree

2 files changed

+272
-1
lines changed

2 files changed

+272
-1
lines changed

pkg/lintcontext/parse_yaml.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import (
2424
"helm.sh/helm/v3/pkg/engine"
2525
autoscalingV2Beta1 "k8s.io/api/autoscaling/v2beta1"
2626
v1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
"k8s.io/apimachinery/pkg/runtime/serializer"
30+
runtimeYaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
2931
"k8s.io/apimachinery/pkg/util/yaml"
3032
"k8s.io/client-go/kubernetes/scheme"
3133
y "sigs.k8s.io/yaml"
@@ -58,7 +60,17 @@ func parseObjects(data []byte, d runtime.Decoder) ([]k8sutil.Object, error) {
5860
}
5961
obj, _, err := d.Decode(data, nil, nil)
6062
if err != nil {
61-
return nil, fmt.Errorf("failed to decode: %w", err)
63+
// this is for backward compatibility, should be replaced with kubeconform
64+
if strings.Contains(err.Error(), "json: cannot unmarshal") {
65+
return nil, fmt.Errorf("failed to decode: %w", err)
66+
}
67+
// fallback to unstructured as schema validation will be performed by kubeconform check
68+
dec := runtimeYaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
69+
var unstructuredErr error
70+
obj, _, unstructuredErr = dec.Decode(data, nil, obj)
71+
if unstructuredErr != nil {
72+
return nil, fmt.Errorf("failed to decode: %w: %w", err, unstructuredErr)
73+
}
6274
}
6375
if list, ok := obj.(*v1.List); ok {
6476
objs := make([]k8sutil.Object, 0, len(list.Items))

pkg/lintcontext/parse_yaml_test.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package lintcontext
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
corev1 "k8s.io/api/core/v1"
9+
)
10+
11+
func TestParseObjects(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
yamlData string
15+
expectError bool
16+
expectCount int
17+
expectKind string
18+
expectName string
19+
}{
20+
{
21+
name: "valid Pod",
22+
yamlData: `apiVersion: v1
23+
kind: Pod
24+
metadata:
25+
name: test-pod
26+
namespace: default
27+
spec:
28+
containers:
29+
- name: nginx
30+
image: nginx:latest
31+
ports:
32+
- containerPort: 80`,
33+
expectError: false,
34+
expectCount: 1,
35+
expectKind: "Pod",
36+
expectName: "test-pod",
37+
},
38+
{
39+
name: "valid Service",
40+
yamlData: `apiVersion: v1
41+
kind: Service
42+
metadata:
43+
name: test-service
44+
namespace: default
45+
spec:
46+
selector:
47+
app: nginx
48+
ports:
49+
- port: 80
50+
targetPort: 80
51+
type: ClusterIP`,
52+
expectError: false,
53+
expectCount: 1,
54+
expectKind: "Service",
55+
expectName: "test-service",
56+
},
57+
{
58+
name: "Tekton Task CRD",
59+
yamlData: `apiVersion: tekton.dev/v1
60+
kind: Task
61+
metadata:
62+
name: hello-world-task
63+
namespace: default
64+
spec:
65+
description: A simple hello world task
66+
steps:
67+
- name: hello
68+
image: alpine:latest
69+
command:
70+
- echo
71+
args:
72+
- "Hello World!"`,
73+
expectError: false,
74+
expectCount: 1,
75+
expectKind: "Task",
76+
expectName: "hello-world-task",
77+
},
78+
{
79+
name: "List with multiple objects",
80+
yamlData: `apiVersion: v1
81+
kind: List
82+
metadata: {}
83+
items:
84+
- apiVersion: v1
85+
kind: Pod
86+
metadata:
87+
name: pod1
88+
spec:
89+
containers:
90+
- name: nginx
91+
image: nginx:latest
92+
- apiVersion: v1
93+
kind: Service
94+
metadata:
95+
name: service1
96+
spec:
97+
selector:
98+
app: nginx
99+
ports:
100+
- port: 80`,
101+
expectError: false,
102+
expectCount: 2,
103+
expectKind: "Pod", // First object
104+
expectName: "pod1",
105+
},
106+
{
107+
name: "invalid YAML",
108+
yamlData: `apiVersion: v1
109+
kind: Pod
110+
metadata:
111+
name: test-pod
112+
spec:
113+
invalidField: this-should-not-be-here
114+
containers:
115+
- name: nginx
116+
image: nginx:latest
117+
invalidContainerField: also-invalid`,
118+
expectError: false, // parseObjects doesn't validate schema, only structure
119+
expectCount: 1,
120+
expectKind: "Pod",
121+
expectName: "test-pod",
122+
},
123+
{
124+
name: "malformed YAML",
125+
yamlData: `apiVersion: v1
126+
kind: Pod
127+
metadata:
128+
name: test-pod
129+
spec:
130+
containers:
131+
- name: nginx
132+
image: nginx:latest
133+
ports:
134+
- containerPort: "invalid-port-type"`, // string instead of int
135+
expectError: true, // Should fail due to type mismatch
136+
expectCount: 0,
137+
expectKind: "",
138+
expectName: "",
139+
},
140+
{
141+
name: "unknown Kubernetes resource type",
142+
yamlData: `apiVersion: example.com/v1
143+
kind: CustomResource
144+
metadata:
145+
name: test-custom
146+
namespace: default
147+
spec:
148+
customField: value`,
149+
expectError: false,
150+
expectCount: 1,
151+
expectKind: "CustomResource",
152+
expectName: "test-custom",
153+
},
154+
}
155+
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
objects, err := parseObjects([]byte(tt.yamlData), nil)
159+
160+
if tt.expectError {
161+
assert.Error(t, err, "Expected parseObjects to return an error")
162+
assert.Len(t, objects, tt.expectCount)
163+
} else {
164+
assert.NoError(t, err, "Expected parseObjects to succeed")
165+
require.Len(t, objects, tt.expectCount, "Expected specific number of objects")
166+
167+
if tt.expectCount > 0 {
168+
// Check first object
169+
firstObj := objects[0]
170+
assert.Equal(t, tt.expectKind, firstObj.GetObjectKind().GroupVersionKind().Kind)
171+
assert.Equal(t, tt.expectName, firstObj.GetName())
172+
173+
// Additional validation for Pod objects
174+
if tt.expectKind == "Pod" {
175+
pod, ok := firstObj.(*corev1.Pod)
176+
require.True(t, ok, "Expected object to be a Pod")
177+
assert.Equal(t, "v1", pod.APIVersion)
178+
assert.Equal(t, "Pod", pod.Kind)
179+
assert.NotEmpty(t, pod.Spec.Containers, "Expected Pod to have containers")
180+
}
181+
}
182+
}
183+
})
184+
}
185+
}
186+
187+
func TestParseObjectsWithCustomDecoder(t *testing.T) {
188+
// Test that parseObjects respects the custom decoder parameter
189+
tektonTaskYAML := `apiVersion: tekton.dev/v1
190+
kind: Task
191+
metadata:
192+
name: hello-world-task
193+
spec:
194+
description: A simple hello world task
195+
steps:
196+
- name: hello
197+
image: alpine:latest
198+
command:
199+
- echo
200+
args:
201+
- "Hello World!"`
202+
203+
// Test with default decoder (should fail)
204+
objects, err := parseObjects([]byte(tektonTaskYAML), nil)
205+
assert.Error(t, err, "Expected Tekton Task to fail with default decoder")
206+
assert.Empty(t, objects)
207+
208+
// Test with explicit decoder (should also fail since we're using the same decoder)
209+
objects, err = parseObjects([]byte(tektonTaskYAML), decoder)
210+
assert.Error(t, err, "Expected Tekton Task to fail with current scheme")
211+
assert.Empty(t, objects)
212+
}
213+
214+
func TestParseObjectsEmptyInput(t *testing.T) {
215+
// Test empty input
216+
objects, err := parseObjects([]byte(""), nil)
217+
assert.Error(t, err, "Expected empty input to return an error")
218+
assert.Empty(t, objects)
219+
220+
// Test whitespace only
221+
objects, err = parseObjects([]byte(" \n \t \n"), nil)
222+
assert.Error(t, err, "Expected whitespace-only input to return an error")
223+
assert.Empty(t, objects)
224+
}
225+
226+
func TestParseObjectsValidateObjectInterface(t *testing.T) {
227+
// Test that parsed objects implement the k8sutil.Object interface correctly
228+
podYAML := `apiVersion: v1
229+
kind: Pod
230+
metadata:
231+
name: test-pod
232+
namespace: test-namespace
233+
labels:
234+
app: test
235+
annotations:
236+
test: annotation
237+
spec:
238+
containers:
239+
- name: nginx
240+
image: nginx:latest`
241+
242+
objects, err := parseObjects([]byte(podYAML), nil)
243+
require.NoError(t, err)
244+
require.Len(t, objects, 1)
245+
246+
pod := objects[0]
247+
248+
// Test Object interface methods
249+
assert.Equal(t, "test-pod", pod.GetName())
250+
assert.Equal(t, "test-namespace", pod.GetNamespace())
251+
assert.Equal(t, map[string]string{"app": "test"}, pod.GetLabels())
252+
assert.Equal(t, map[string]string{"test": "annotation"}, pod.GetAnnotations())
253+
254+
// Test GroupVersionKind
255+
gvk := pod.GetObjectKind().GroupVersionKind()
256+
assert.Equal(t, "", gvk.Group)
257+
assert.Equal(t, "v1", gvk.Version)
258+
assert.Equal(t, "Pod", gvk.Kind)
259+
}

0 commit comments

Comments
 (0)