Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate ObjectMeta #288

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
35 changes: 35 additions & 0 deletions acceptance.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down
8 changes: 8 additions & 0 deletions fixtures/annotation_key_invalid.yaml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 8 additions & 0 deletions fixtures/annotation_missing_value.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
annotations:
some.domain/some-key:
data:
file.name: "a value"
8 changes: 8 additions & 0 deletions fixtures/annotation_null_value.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: some-values
annotations:
some.domain/some-key: null
data:
file.name: "a value"
2 changes: 1 addition & 1 deletion fixtures/generate_name.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: batch/v1
kind: Job
metadata:
generateName: pi-
generateName: pi
spec:
template:
spec:
Expand Down
8 changes: 8 additions & 0 deletions fixtures/label_name_length.yaml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 8 additions & 0 deletions fixtures/label_namespace.yaml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 8 additions & 0 deletions fixtures/label_value_length.yaml
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 4 additions & 0 deletions fixtures/metadata_missing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
data:
file.name: "a value"
6 changes: 6 additions & 0 deletions fixtures/metadata_name_missing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:

data:
file.name: "a value"
6 changes: 6 additions & 0 deletions fixtures/object_name-max_length.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: abcdefghijklmnopqrstuvwxyz-01234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ
data:
file.name: "a value"
5 changes: 5 additions & 0 deletions output.xml
Original file line number Diff line number Diff line change
@@ -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>
17 changes: 12 additions & 5 deletions pkg/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ 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
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.
Expand All @@ -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 != "" {
Expand Down
157 changes: 155 additions & 2 deletions pkg/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"errors"
"fmt"
"io"
"regexp"
"strings"

jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
Expand Down Expand Up @@ -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 {
Expand All @@ -202,7 +204,6 @@ func (val *v) ValidateResource(res resource.Resource) Result {
Msg: ve.Message,
})
}

}
return Result{
Resource: res,
Expand All @@ -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}
}

Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
`)

Expand Down