cli-utils/pkg/object/validation/validate_test.go

278 lines
6.5 KiB
Go

// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package validation_test
import (
"testing"
"github.com/fluxcd/cli-utils/pkg/multierror"
"github.com/fluxcd/cli-utils/pkg/object"
"github.com/fluxcd/cli-utils/pkg/object/validation"
"github.com/fluxcd/cli-utils/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
)
func TestValidate(t *testing.T) {
testCases := map[string]struct {
resources []*unstructured.Unstructured
expectedError error
}{
"missing kind": {
resources: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"metadata": map[string]interface{}{
"name": "foo",
"namespace": "default",
},
},
},
},
expectedError: validation.NewError(
&field.Error{
Type: field.ErrorTypeRequired,
Field: "kind",
BadValue: "",
Detail: "kind is required",
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "",
},
Name: "foo",
Namespace: "default",
},
),
},
"multiple errors in one object": {
resources: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
},
},
},
expectedError: validation.NewError(
multierror.New(
&field.Error{
Type: field.ErrorTypeRequired,
Field: "metadata.name",
BadValue: "",
Detail: "name is required",
},
&field.Error{
Type: field.ErrorTypeRequired,
Field: "metadata.namespace",
BadValue: "",
Detail: "namespace is required",
},
),
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "",
Namespace: "",
},
),
},
"one error in multiple object": {
resources: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"namespace": "default",
},
},
},
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"namespace": "default",
},
},
},
},
expectedError: multierror.New(
validation.NewError(
&field.Error{
Type: field.ErrorTypeRequired,
Field: "metadata.name",
BadValue: "",
Detail: "name is required",
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "",
Namespace: "default",
},
),
validation.NewError(
&field.Error{
Type: field.ErrorTypeRequired,
Field: "metadata.name",
BadValue: "",
Detail: "name is required",
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "StatefulSet",
},
Name: "",
Namespace: "default",
},
),
),
},
"namespace must be empty (cluster-scoped)": {
resources: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{
"name": "foo",
"namespace": "default",
},
},
},
},
expectedError: validation.NewError(
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "metadata.namespace",
BadValue: "default",
Detail: "namespace must be empty",
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Namespace",
},
Name: "foo",
Namespace: "default",
},
),
},
"namespace is required (namespace-scoped)": {
resources: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo",
},
},
},
},
expectedError: validation.NewError(
&field.Error{
Type: field.ErrorTypeRequired,
Field: "metadata.namespace",
BadValue: "",
Detail: "namespace is required",
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "",
},
),
},
"scope for CRs are found in CRDs if available": {
resources: []*unstructured.Unstructured{
testutil.Unstructured(t, `
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: customs.custom.io
spec:
group: custom.io
names:
kind: Custom
scope: Cluster
versions:
- name: v1
`,
),
testutil.Unstructured(t, `
apiVersion: custom.io/v1
kind: Custom
metadata:
name: foo
namespace: default
`,
),
},
expectedError: validation.NewError(
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "metadata.namespace",
BadValue: "default",
Detail: "namespace must be empty",
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "custom.io",
Kind: "Custom",
},
Name: "foo",
Namespace: "default",
},
),
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test-ns")
defer tf.Cleanup()
mapper, err := tf.ToRESTMapper()
require.NoError(t, err)
crdGV := schema.GroupVersion{Group: "apiextensions.k8s.io", Version: "v1"}
crdMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{crdGV})
crdMapper.AddSpecific(crdGV.WithKind("CustomResourceDefinition"),
crdGV.WithResource("customresourcedefinitions"),
crdGV.WithResource("customresourcedefinition"), meta.RESTScopeRoot)
mapper = meta.MultiRESTMapper([]meta.RESTMapper{mapper, crdMapper})
vCollector := &validation.Collector{}
validator := &validation.Validator{
Mapper: mapper,
Collector: vCollector,
}
validator.Validate(tc.resources)
err = vCollector.ToError()
if tc.expectedError == nil {
assert.NoError(t, err)
return
}
require.EqualError(t, err, tc.expectedError.Error())
})
}
}