This commit is contained in:
Chas Honton 2024-08-29 10:32:18 -07:00 committed by GitHub
commit 4f53a670e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 276 additions and 8 deletions

View file

@ -81,6 +81,41 @@ resetCacheFolder() {
[ "$status" -eq 1 ]
}
@test "Fail when annotation key is invalid" {
run bin/kubeconform fixtures/annotation_key_invalid.yaml
[ "$status" -eq 1 ]
}
@test "Fail when annotation value is missing" {
run bin/kubeconform fixtures/annotation_missing_value.yaml
[ "$status" -eq 1 ]
}
@test "Fail when annotation value is null" {
run bin/kubeconform fixtures/annotation_null_value.yaml
[ "$status" -eq 1 ]
}
@test "Fail when label name is too long" {
run bin/kubeconform fixtures/label_name_length.yaml
[ "$status" -eq 1 ]
}
@test "Fail when label namespace is invalid domain" {
run bin/kubeconform fixtures/label_namespace.yaml
[ "$status" -eq 1 ]
}
@test "Fail when label value is too long" {
run bin/kubeconform fixtures/label_value_length.yaml
[ "$status" -eq 1 ]
}
@test "Fail when metadata name is missing" {
run bin/kubeconform fixtures/metadata_name_missing.yaml
[ "$status" -eq 1 ]
}
@test "Return relevant error for non-existent file" {
run bin/kubeconform fixtures/not-here
[ "$status" -eq 1 ]

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
labels:
cert-manager.io/cluster-issuer": issue #275
data:
file.name: "a value"

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
annotations:
some.domain/some-key:
data:
file.name: "a value"

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
annotations:
some.domain/some-key: null
data:
file.name: "a value"

View file

@ -1,7 +1,7 @@
apiVersion: batch/v1
kind: Job
metadata:
generateName: pi-
generateName: pi
spec:
template:
spec:

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
labels:
abcdefghijklmnopqrstuvwxyz-01234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ: "123456789_123456789_123456789_123456789_123456789_123456789_123"
data:
file.name: "a value"

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
labels:
abcdefghijklmnopqrstuvwxyz-01234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ.example.com/ABCDEFGHIJKLMNOPQRSTUVWXYZ: "123456789_123456789_123456789_123456789_123456789_123456789_123"
data:
file.name: "a value"

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
labels:
some.domain/some-key: 123456789_123456789_123456789_123456789_123456789_123456789_1234
data:
file.name: "a value"

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
data:
file.name: "a value"

View file

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
data:
file.name: "a value"

View file

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: abcdefghijklmnopqrstuvwxyz-01234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ
data:
file.name: "a value"

5
output.xml Normal file
View file

@ -0,0 +1,5 @@
<testsuites name="kubeconform" time="0.14241325" tests="1" failures="0" disabled="0" errors="0">
<testsuite name="fixtures/valid.yaml" id="1" tests="1" failures="0" errors="0" disabled="0" skipped="0">
<testcase name="bob" classname="ReplicationController@v1" time="0"></testcase>
</testsuite>
</testsuites>

View file

@ -13,6 +13,7 @@ type Resource struct {
Bytes []byte
sig *Signature // Cache signature parsing
sigErr error // Cache potential signature parsing error
Metadata *ObjectMeta
}
// Signature is a key representing a Kubernetes resource
@ -20,6 +21,15 @@ type Signature struct {
Kind, Version, Namespace, Name string
}
// Metadata holds Kubernetes resource ObjectMeta
type ObjectMeta struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
GenerateName string `yaml:"generateName"`
Annotations map[string]string `yaml:annotations`
Labels map[string]string `yaml:labels`
}
// GroupVersionKind returns a string with the GVK encoding of a resource signature.
// This encoding slightly differs from the Kubernetes upstream implementation
// in order to be suitable for being used in the kubeconform command-line arguments.
@ -41,13 +51,10 @@ func (res *Resource) Signature() (*Signature, error) {
resource := struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
GenerateName string `yaml:"generateName"`
} `yaml:"Metadata"`
Metadata ObjectMeta `yaml:"metadata"`
}{}
err := yaml.Unmarshal(res.Bytes, &resource)
res.Metadata = &resource.Metadata
name := resource.Metadata.Name
if resource.Metadata.GenerateName != "" {

View file

@ -7,6 +7,8 @@ import (
"errors"
"fmt"
"io"
"regexp"
"strings"
jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
@ -191,9 +193,9 @@ func (val *v) ValidateResource(res resource.Resource) Result {
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
}
validationErrors := []ValidationError{}
err = schema.Validate(r)
if err != nil {
validationErrors := []ValidationError{}
var e *jsonschema.ValidationError
if errors.As(err, &e) {
for _, ve := range e.Causes {
@ -202,7 +204,6 @@ func (val *v) ValidateResource(res resource.Resource) Result {
Msg: ve.Message,
})
}
}
return Result{
Resource: res,
@ -212,6 +213,35 @@ func (val *v) ValidateResource(res resource.Resource) Result {
}
}
if res.Metadata != nil {
metadataPath := res.Path + " - .metadata"
namePath := metadataPath + ".name"
name := res.Metadata.Name
if name == "" {
if res.Metadata.GenerateName != "" {
name = res.Metadata.GenerateName
namePath = metadataPath + ".generateName"
}
}
if !validateDnsLabels(name) {
validationErrors = append(validationErrors, ValidationError{
Path: namePath,
Msg: "invalid metadata name",
})
}
validationErrors = validateMeta(metadataPath, res.Metadata, validationErrors)
if len(validationErrors) > 0 {
return Result{
Resource: res,
Status: Invalid,
Err: fmt.Errorf("invalid metadata."),
ValidationErrors: validationErrors,
}
}
}
return Result{Resource: res, Status: Valid}
}
@ -279,3 +309,126 @@ func downloadSchema(registries []registry.Registry, kind, version, k8sVersion st
return nil, nil // No schema found - we don't consider it an error, resource will be skipped
}
func validateMeta(path string, metadata *resource.ObjectMeta, validationErrors []ValidationError) []ValidationError {
if metadata.Annotations != nil {
validationErrors = validateAnnotations(path + ".annotations", metadata.Annotations, validationErrors)
}
if metadata.Labels != nil {
validationErrors = validateLabels(path + ".labels", metadata.Labels, validationErrors)
}
return validationErrors
}
/* Annotations are key/value pairs. */
func validateAnnotations(path string, annotations map[string]string, validationErrors []ValidationError) []ValidationError {
for k, v := range annotations {
keypath := path + "[" + k + "]"
validationErrors = validateKey(keypath, k, validationErrors)
if !validateAnnotationValue(v) {
validationErrors = append(validationErrors, ValidationError{
Path: keypath,
Msg: "invalid annotation value",
})
}
}
return validationErrors
}
/* Labels are key/value pairs.
*/
func validateLabels(path string, labels map[string]string, validationErrors []ValidationError) []ValidationError {
for k, v := range labels {
keypath := path + "[" + k + "]"
validationErrors= validateKey(keypath, k, validationErrors)
if !validateNameSegment(v) {
validationErrors = append(validationErrors, ValidationError{
Path: keypath,
Msg: "invalid label value",
})
}
}
return validationErrors
}
/* Valid keys have two segments: an optional prefix and name, separated by a slash (/)
*/
func validateKey(keypath string, key string, validationErrors []ValidationError) []ValidationError {
if len(key) == 0 {
validationErrors = append(validationErrors, ValidationError{
Path: keypath,
Msg: "invalid annotation key",
})
} else {
var name string
prefix, suffix, found := strings.Cut(key, "/")
if found {
name = suffix
if !validateDnsLabels(prefix) {
validationErrors = append(validationErrors, ValidationError{
Path: keypath,
Msg: "invalid annotation key prefix",
})
}
} else {
name = key
}
if !validateNameSegment(name) {
validationErrors = append(validationErrors, ValidationError{
Path: keypath,
Msg: "invalid annotation key name",
})
}
}
return validationErrors
}
var alphanumericPlusUnderscorePeriodHyphen = regexp.MustCompile("^[0-9A-Za-z_.-]+$")
func isAlphaNumeric(v byte) bool {
return (v >= '0' && v <= '9') ||
(v >= 'A' && v <= 'Z') ||
(v >= 'a' && v <= 'z')
}
/* The name segment must be 63 characters or less, beginning and ending with an alphanumeric character
([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and alphanumerics between.
*/
func validateNameSegment(name string) bool {
return len(name) <= 63 &&
alphanumericPlusUnderscorePeriodHyphen.MatchString(name) &&
isAlphaNumeric(name[0]) &&
isAlphaNumeric(name[len(name)-1])
}
var alphanumericPlusHyphen = regexp.MustCompile("^[0-9A-Za-z-]+$")
/* The domain name may not exceed the length of 253 characters in its textual representation.
A label may contain one to 63 characters of a through z, A through Z, digits 0 through 9, and hyphen.
Labels may not start or end with a hyphen.
*/
func validateDnsLabels(domain string) bool {
if len(domain) == 0 || len(domain) > 253 {
return false
} else {
labels := strings.Split(domain, ".")
for _, label := range labels {
if len(label) == 0 ||
len(label) > 63 ||
!alphanumericPlusHyphen.MatchString(label) ||
label[0] == '-' ||
label[len(label)-1] == '-' {
return false
}
}
}
return true
}
/* annotation must have value
*/
func validateAnnotationValue(value string) bool {
return len(value) != 0
}

View file

@ -475,11 +475,15 @@ func TestValidateFile(t *testing.T) {
inputData := []byte(`
kind: name
apiVersion: v1
metadata:
name: bar.qux
firstName: bar
lastName: qux
---
kind: name
apiVersion: v1
metadata:
name: foo
firstName: foo
`)