Merge pull request #8 from yannh/minor-fixes-and-tests

Minor fixes and tests
This commit is contained in:
Yann Hamon 2020-10-18 14:09:31 +02:00 committed by GitHub
commit 98b8de29ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 555 additions and 39 deletions

View file

@ -12,16 +12,28 @@
[ "$output" = "Summary: 7 resources found in 2 files - Valid: 7, Invalid: 0, Errors: 0 Skipped: 0" ]
}
@test "Pass when parsing a valid Kubernetes config file with int_to_string vars" {
run bin/kubeconform -verbose fixtures/int_or_string.yaml
[ "$status" -eq 0 ]
[ "$output" = "fixtures/int_or_string.yaml - Service heapster is valid" ]
}
@test "Pass when parsing a valid Kubernetes config YAML file with generate name" {
run bin/kubeconform -verbose fixtures/generate_name.yaml
[ "$status" -eq 0 ]
[ "$output" = "fixtures/generate_name.yaml - Job pi-{{ generateName }} is valid" ]
}
@test "Pass when parsing a Kubernetes file with string and integer quantities" {
run bin/kubeconform -verbose fixtures/quantity.yaml
[ "$status" -eq 0 ]
[ "$output" = "fixtures/quantity.yaml - LimitRange is valid" ]
[ "$output" = "fixtures/quantity.yaml - LimitRange mem-limit-range is valid" ]
}
@test "Pass when parsing a valid Kubernetes config file with null arrays" {
run bin/kubeconform -verbose fixtures/null_string.yaml
[ "$status" -eq 0 ]
[ "$output" = "fixtures/null_string.yaml - Service is valid" ]
[ "$output" = "fixtures/null_string.yaml - Service frontend is valid" ]
}
@test "Pass when parsing a multi-document config file" {
@ -43,7 +55,19 @@
@test "Return relevant error for non-existent file" {
run bin/kubeconform fixtures/not-here
[ "$status" -eq 1 ]
[ "$output" = "fixtures/not-here - failed validation: open fixtures/not-here: no such file or directory" ]
[ "$output" = "fixtures/not-here - failed validation: open fixtures/not-here: no such file or directory" ]
}
@test "Pass when parsing a blank config file" {
run bin/kubeconform -summary fixtures/blank.yaml
[ "$status" -eq 0 ]
[ "$output" = "Summary: 0 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0 Skipped: 0" ]
}
@test "Pass when parsing a blank config file with a comment" {
run bin/kubeconform -summary fixtures/comment.yaml
[ "$status" -eq 0 ]
[ "$output" = "Summary: 0 resource found in 1 file - Valid: 0, Invalid: 0, Errors: 0 Skipped: 0" ]
}
@test "Fail when parsing a config with additional properties and strict set" {
@ -65,3 +89,29 @@
run bin/kubeconform -registry './fixtures/registry/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/test_crd.yaml
[ "$status" -eq 0 ]
}
@test "Pass when parsing a config with additional properties" {
run bin/kubeconform -summary fixtures/extra_property.yaml
[ "$status" -eq 0 ]
[ "$output" = "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0" ]
}
@test "Pass when using a valid, preset --registry" {
run bin/kubeconform --registry kubernetesjsonschema.dev fixtures/valid.yaml
[ "$status" -eq 0 ]
}
@test "Pass when using a valid HTTP --registry" {
run bin/kubeconform --registry 'https://kubernetesjsonschema.dev/{{ .NormalizedVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json' fixtures/valid.yaml
[ "$status" -eq 0 ]
}
@test "Fail when using an invalid HTTP --registry" {
run bin/kubeconform --registry 'http://foo' fixtures/valid.yaml
[ "$status" -eq 1 ]
}
@test "Fail when using an invalid non-HTTP --registry" {
run bin/kubeconform --registry 'foo' fixtures/valid.yaml
[ "$status" -eq 1 ]
}

View file

@ -20,9 +20,9 @@ import (
)
type validationResult struct {
filename, kind, version string
err error
skipped bool
filename, kind, version, Name string
err error
skipped bool
}
func resourcesFromReader(r io.Reader) ([][]byte, error) {
@ -65,21 +65,27 @@ func ValidateStream(r io.Reader, regs []registry.Registry, k8sVersion string, c
return []validationResult{{err: fmt.Errorf("failed reading file: %s", err)}}
}
validationResults := []validationResult{}
if len(rawResources) == 0 {
// In case a file has no resources, we want to capture that the file was parsed - and therefore send a message with an empty resource and no error
validationResults = append(validationResults, validationResult{kind: "", version: "", Name: "", err: nil, skipped: false})
}
for _, rawResource := range rawResources {
var sig resource.Signature
if sig, err = resource.SignatureFromBytes(rawResource); err != nil {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, err: fmt.Errorf("error while parsing: %s", err)})
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: fmt.Errorf("error while parsing: %s", err)})
continue
}
if sig.Kind == "" {
validationResults = append(validationResults, validationResult{kind: "", version: "", Name: "", err: nil, skipped: false})
continue // We skip resoures that don't have a Kind defined
}
if skip(sig) {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, err: nil, skipped: true})
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: nil, skipped: true})
continue
}
@ -95,7 +101,7 @@ func ValidateStream(r io.Reader, regs []registry.Registry, k8sVersion string, c
if !ok {
schema, err = downloadSchema(regs, sig.Kind, sig.Version, k8sVersion)
if err != nil {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, err: err, skipped: false})
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: err, skipped: false})
continue
}
@ -106,14 +112,14 @@ func ValidateStream(r io.Reader, regs []registry.Registry, k8sVersion string, c
if schema == nil {
if ignoreMissingSchemas {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, err: nil, skipped: true})
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: nil, skipped: true})
} else {
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, err: fmt.Errorf("could not find schema for %s", sig.Kind), skipped: false})
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: fmt.Errorf("could not find schema for %s", sig.Kind), skipped: false})
}
}
err = validator.Validate(rawResource, schema)
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, err: err})
validationResults = append(validationResults, validationResult{kind: sig.Kind, version: sig.Version, Name: sig.Name, err: err})
}
return validationResults
@ -162,7 +168,7 @@ func processResults(o output.Output, validationResults chan []validationResult,
success = false
}
if err := o.Write(result.filename, result.kind, result.version, result.err, result.skipped); err != nil {
if err := o.Write(result.filename, result.kind, result.Name, result.version, result.err, result.skipped); err != nil {
fmt.Fprint(os.Stderr, "failed writing log\n")
}
}

View file

@ -1,10 +1,13 @@
---
apiVersion: extensions/v1beta1
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-ds
spec:
replicas: 2
selector:
matchLabels:
k8s-app: nginx-ds
template:
spec:
containers:

View file

@ -0,0 +1,13 @@
apiVersion: batch/v1
kind: Job
metadata:
generateName: pi-
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4

View file

@ -1,5 +1,5 @@
kind: Deployment
apiVersion: extensions/v1beta1
apiVersion: apps/v1
metadata:
labels:
k8s-app: kubernetes-dashboard

View file

@ -0,0 +1,418 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.3.0
creationTimestamp: null
name: trainingjobs.sagemaker.aws.amazon.com
spec:
additionalPrinterColumns:
- JSONPath: .status.trainingJobStatus
name: Status
type: string
- JSONPath: .status.secondaryStatus
name: Secondary-Status
type: string
- JSONPath: .metadata.creationTimestamp
format: date
name: Creation-Time
type: string
- JSONPath: .status.sageMakerTrainingJobName
name: Sagemaker-Job-Name
type: string
group: sagemaker.aws.amazon.com
names:
kind: TrainingJob
listKind: TrainingJobList
plural: trainingjobs
singular: trainingjob
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
description: TrainingJob is the Schema for the trainingjobs API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: TrainingJobSpec defines the desired state of TrainingJob
properties:
algorithmSpecification:
properties:
algorithmName:
minLength: 1
type: string
metricDefinitions:
items:
properties:
name:
minLength: 1
type: string
regex:
minLength: 1
type: string
required:
- name
- regex
type: object
type: array
trainingImage:
minLength: 1
type: string
trainingInputMode:
enum:
- File
- Pipe
type: string
required:
- trainingInputMode
type: object
checkpointConfig:
properties:
localPath:
type: string
s3Uri:
pattern: ^(https|s3)://([^/]+)/?(.*)$
type: string
required:
- s3Uri
type: object
debugHookConfig:
description: DebugHookConfig https://docs.aws.amazon.com/sagemaker/latest/dg/API_DebugHookConfig.html
properties:
collectionConfigurations:
items:
description: CollectionConfiguration https://docs.aws.amazon.com/sagemaker/latest/dg/API_CollectionConfiguration.html
properties:
collectionName:
type: string
collectionParameters:
items:
description: Used in describing maps in Kubernetes.
properties:
name:
type: string
value:
type: string
type: object
type: array
type: object
type: array
localPath:
type: string
ruleParameters:
items:
description: Used in describing maps in Kubernetes.
properties:
name:
type: string
value:
type: string
type: object
type: array
s3OutputPath:
pattern: ^(https|s3)://([^/]+)/?(.*)$
type: string
required:
- s3OutputPath
type: object
debugRuleConfigurations:
items:
description: DebugRuleConfiguration https://docs.aws.amazon.com/sagemaker/latest/dg/API_DebugRuleConfiguration.html
properties:
instanceType:
type: string
localPath:
type: string
ruleConfigurationName:
type: string
ruleEvaluatorImage:
type: string
ruleParameters:
items:
description: Used in describing maps in Kubernetes.
properties:
name:
type: string
value:
type: string
type: object
type: array
s3OutputPath:
pattern: ^(https|s3)://([^/]+)/?(.*)$
type: string
volumeSizeInGB:
format: int64
minimum: 1
type: integer
required:
- ruleConfigurationName
- ruleEvaluatorImage
type: object
type: array
enableInterContainerTrafficEncryption:
type: boolean
enableManagedSpotTraining:
type: boolean
enableNetworkIsolation:
type: boolean
hyperParameters:
items:
description: Used in describing maps in Kubernetes.
properties:
name:
type: string
value:
type: string
type: object
type: array
inputDataConfig:
items:
properties:
channelName:
minLength: 1
pattern: '[A-Za-z0-9\.\-_]+'
type: string
compressionType:
enum:
- None
- Gzip
type: string
contentType:
type: string
dataSource:
properties:
fileSystemDataSource:
properties:
directoryPath:
type: string
fileSystemAccessMode:
type: string
fileSystemId:
type: string
fileSystemType:
type: string
required:
- directoryPath
- fileSystemAccessMode
- fileSystemId
- fileSystemType
type: object
s3DataSource:
properties:
attributeNames:
items:
type: string
type: array
s3DataDistributionType:
enum:
- FullyReplicated
- ShardedByS3Key
type: string
s3DataType:
enum:
- S3Prefix
- ManifestFile
- AugmentedManifestFile
type: string
s3Uri:
pattern: ^(https|s3)://([^/]+)/?(.*)$
type: string
required:
- s3DataType
- s3Uri
type: object
type: object
inputMode:
enum:
- Pipe
- File
type: string
recordWrapperType:
type: string
shuffleConfig:
properties:
seed:
format: int64
type: integer
required:
- seed
type: object
required:
- channelName
- dataSource
type: object
minItems: 1
type: array
outputDataConfig:
properties:
kmsKeyId:
type: string
s3OutputPath:
pattern: ^(https|s3)://([^/]+)/?(.*)$
type: string
required:
- s3OutputPath
type: object
region:
minLength: 1
type: string
resourceConfig:
properties:
instanceCount:
format: int64
minimum: 1
type: integer
instanceType:
minLength: 1
type: string
volumeKmsKeyId:
type: string
volumeSizeInGB:
format: int64
minimum: 1
type: integer
required:
- instanceCount
- instanceType
- volumeSizeInGB
type: object
roleArn:
minLength: 20
type: string
sageMakerEndpoint:
description: A custom SageMaker endpoint to use when communicating with SageMaker.
pattern: ^(https|http)://.*$
type: string
stoppingCondition:
properties:
maxRuntimeInSeconds:
format: int64
minimum: 1
type: integer
maxWaitTimeInSeconds:
format: int64
minimum: 1
type: integer
type: object
tags:
items:
properties:
key:
minLength: 1
type: string
value:
type: string
required:
- key
- value
type: object
type: array
tensorBoardOutputConfig:
description: TensorBoardOutputConfig https://docs.aws.amazon.com/sagemaker/latest/dg/API_TensorBoardOutputConfig.html
properties:
localPath:
type: string
s3OutputPath:
pattern: ^(https|s3)://([^/]+)/?(.*)$
type: string
required:
- s3OutputPath
type: object
trainingJobName:
description: The SageMaker training job name. This is optional for the SageMaker K8s operator. If it is empty, the operator will populate it with a generated name.
maxLength: 63
type: string
vpcConfig:
properties:
securityGroupIds:
items:
type: string
maxItems: 5
minItems: 1
type: array
subnets:
items:
type: string
maxItems: 16
minItems: 1
type: array
required:
- securityGroupIds
- subnets
type: object
required:
- algorithmSpecification
- outputDataConfig
- region
- resourceConfig
- roleArn
- stoppingCondition
type: object
status:
description: TrainingJobStatus defines the observed state of TrainingJob
properties:
additional:
description: Field to store additional information, for example if we are unable to check the status we update this.
type: string
cloudWatchLogUrl:
description: Cloud Watch url for training log
type: string
debugRuleEvaluationStatuses:
description: Status of rule evaluation jobs, obtained from DebugRuleEvaluationStatuses. https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_DescribeTrainingJob.html#sagemaker-DescribeTrainingJob-response-DebugRuleEvaluationStatuses
items:
description: DebugRuleEvaluationStatus https://docs.aws.amazon.com/sagemaker/latest/dg/API_DebugRuleEvaluationStatus.html
properties:
lastModifiedTime:
format: date-time
type: string
ruleConfigurationName:
type: string
ruleEvaluationJobArn:
type: string
ruleEvaluationStatus:
type: string
statusDetail:
type: string
type: object
type: array
lastCheckTime:
description: The last time that we checked the status of the SageMaker job.
format: date-time
type: string
modelPath:
description: Full path to the training artifact (model)
type: string
sageMakerTrainingJobName:
description: SageMaker training job name
type: string
secondaryStatus:
description: The secondary, more granular status of the training job. https://docs.aws.amazon.com/sagemaker/latest/dg/API_DescribeTrainingJob.html#SageMaker-DescribeTrainingJob-response-SecondaryStatus
type: string
trainingJobStatus:
description: The status of the training job. https://docs.aws.amazon.com/sagemaker/latest/dg/API_DescribeTrainingJob.html#SageMaker-DescribeTrainingJob-response-TrainingJobStatus
type: string
type: object
required:
- spec
type: object
version: v1
versions:
- name: v1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View file

@ -9,6 +9,7 @@ import (
type result struct {
Filename string `json:"filename"`
Kind string `json:"kind"`
Name string `json:"name"`
Version string `json:"version"`
Status string `json:"status"`
Msg string `json:"msg"`
@ -37,10 +38,10 @@ func JSON(w io.Writer, withSummary bool, verbose bool) Output {
}
// JSON.Write will only write when JSON.Flush has been called
func (o *jsono) Write(filename, kind, version string, err error, skipped bool) error {
func (o *jsono) Write(filename, kind, name, version string, err error, skipped bool) error {
msg, st := "", ""
s := status(err, skipped)
s := status(kind, name, err, skipped)
switch s {
case VALID:
@ -57,10 +58,11 @@ func (o *jsono) Write(filename, kind, version string, err error, skipped bool) e
case SKIPPED:
st = "SKIPPED"
o.nSkipped++
case EMPTY:
}
if o.verbose || (s != VALID && s != SKIPPED) {
o.results = append(o.results, result{Filename: filename, Kind: kind, Version: version, Status: st, Msg: msg})
if o.verbose || (s != VALID && s != SKIPPED && s != EMPTY ) {
o.results = append(o.results, result{Filename: filename, Kind: kind, Name: name, Version: version, Status: st, Msg: msg})
}
return nil

View file

@ -7,9 +7,9 @@ import (
func TestJSONWrite(t *testing.T) {
type result struct {
fileName, kind, version string
err error
skipped bool
fileName, kind, name, version string
err error
skipped bool
}
for _, testCase := range []struct {
@ -28,6 +28,7 @@ func TestJSONWrite(t *testing.T) {
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
@ -46,6 +47,7 @@ func TestJSONWrite(t *testing.T) {
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
@ -70,6 +72,7 @@ func TestJSONWrite(t *testing.T) {
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
@ -80,6 +83,7 @@ func TestJSONWrite(t *testing.T) {
{
"filename": "deployment.yml",
"kind": "Deployment",
"name": "my-app",
"version": "apps/v1",
"status": "VALID",
"msg": ""
@ -99,7 +103,7 @@ func TestJSONWrite(t *testing.T) {
o := JSON(w, testCase.withSummary, testCase.verbose)
for _, res := range testCase.res {
o.Write(res.fileName, res.kind, res.version, res.err, res.skipped)
o.Write(res.fileName, res.kind, res.name, res.version, res.err, res.skipped)
}
o.Flush()

View file

@ -10,14 +10,19 @@ const (
INVALID
ERROR
SKIPPED
EMPTY
)
type Output interface {
Write(filename, kind, version string, err error, skipped bool) error
Write(filename, kind, name, version string, err error, skipped bool) error
Flush() error
}
func status(err error, skipped bool) int {
func status(kind, name string, err error, skipped bool) int {
if name == "" && kind == "" && err == nil && skipped == false {
return EMPTY
}
if skipped {
return SKIPPED
}

View file

@ -29,30 +29,35 @@ func Text(w io.Writer, withSummary, verbose bool) Output {
}
}
func (o *text) Write(filename, kind, version string, reserr error, skipped bool) error {
func (o *text) Write(filename, kind, name, version string, reserr error, skipped bool) error {
o.Lock()
defer o.Unlock()
var err error
o.files[filename] = true
switch status(reserr, skipped) {
switch status(kind, name, reserr, skipped) {
case VALID:
if o.verbose {
_, err = fmt.Fprintf(o.w, "%s - %s is valid\n", filename, kind)
_, err = fmt.Fprintf(o.w, "%s - %s %s is valid\n", filename, kind, name)
}
o.nValid++
case INVALID:
_, err = fmt.Fprintf(o.w, "%s - %s is invalid: %s\n", filename, kind, reserr)
_, err = fmt.Fprintf(o.w, "%s - %s %s is invalid: %s\n", filename, kind, name, reserr)
o.nInvalid++
case ERROR:
_, err = fmt.Fprintf(o.w, "%s - %s failed validation: %s\n", filename, kind, reserr)
if kind != "" && name != "" {
_, err = fmt.Fprintf(o.w, "%s - %s %s failed validation: %s\n", filename, kind, name, reserr)
} else {
_, err = fmt.Fprintf(o.w, "%s - failed validation: %s\n", filename, reserr)
}
o.nErrors++
case SKIPPED:
if o.verbose {
_, err = fmt.Fprintf(o.w, "%s - %s skipped\n", filename, kind)
_, err = fmt.Fprintf(o.w, "%s - %s %s skipped\n", filename, name, kind)
}
o.nSkipped++
case EMPTY: // sent to ensure we count the filename as parsed
}
return err

View file

@ -7,9 +7,9 @@ import (
func TestTextWrite(t *testing.T) {
type result struct {
fileName, kind, version string
err error
skipped bool
fileName, kind, name, version string
err error
skipped bool
}
for _, testCase := range []struct {
@ -28,6 +28,7 @@ func TestTextWrite(t *testing.T) {
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
@ -43,6 +44,7 @@ func TestTextWrite(t *testing.T) {
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
@ -58,12 +60,13 @@ func TestTextWrite(t *testing.T) {
{
"deployment.yml",
"Deployment",
"my-app",
"apps/v1",
nil,
false,
},
},
`deployment.yml - Deployment is valid
`deployment.yml - Deployment my-app is valid
Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
`,
},
@ -72,7 +75,7 @@ Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0
o := Text(w, testCase.withSummary, testCase.verbose)
for _, res := range testCase.res {
o.Write(res.fileName, res.kind, res.version, res.err, res.skipped)
o.Write(res.fileName, res.kind, res.name, res.version, res.err, res.skipped)
}
o.Flush()

View file

@ -5,7 +5,7 @@ import (
)
type Signature struct {
Kind, Version, Namespace string
Kind, Version, Namespace, Name string
}
// SignatureFromBytes returns key identifying elements from a []byte representing the resource
@ -14,10 +14,17 @@ func SignatureFromBytes(res []byte) (Signature, error) {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata struct {
Namespace string `yaml:"Namespace"`
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
GenerateName string `yaml:"generateName"`
} `yaml:"Metadata"`
}{}
err := yaml.Unmarshal(res, &resource)
return Signature{Kind: resource.Kind, Version: resource.APIVersion, Namespace: resource.Metadata.Namespace}, err
name := resource.Metadata.Name
if resource.Metadata.GenerateName != "" {
name = resource.Metadata.GenerateName + "{{ generateName }}"
}
return Signature{Kind: resource.Kind, Version: resource.APIVersion, Namespace: resource.Metadata.Namespace, Name: name}, err
}