mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-11 14:09:21 +00:00
Merge 77c022cfa1 into 1bd44986dd
This commit is contained in:
commit
17b2fd074f
19 changed files with 430 additions and 22 deletions
|
|
@ -81,6 +81,56 @@ 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 "Pass if skip-metadata added" {
|
||||
run bin/kubeconform -skip-metadata fixtures/metadata_name_missing.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Pass with extra metadata fields" {
|
||||
run bin/kubeconform fixtures/metadata_extra.yaml
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Fail extra metadata fields when strict" {
|
||||
run bin/kubeconform -strict fixtures/metadata_extra.yaml
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "Return relevant error for non-existent file" {
|
||||
run bin/kubeconform fixtures/not-here
|
||||
[ "$status" -eq 1 ]
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ func kubeconform(cfg config.Config) int {
|
|||
SkipKinds: cfg.SkipKinds,
|
||||
RejectKinds: cfg.RejectKinds,
|
||||
KubernetesVersion: cfg.KubernetesVersion.String(),
|
||||
SkipMetadata: cfg.SkipMetadata,
|
||||
Strict: cfg.Strict,
|
||||
IgnoreMissingSchemas: cfg.IgnoreMissingSchemas,
|
||||
})
|
||||
|
|
|
|||
8
fixtures/annotation_key_invalid.yaml
Normal file
8
fixtures/annotation_key_invalid.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: some-values
|
||||
annotations:
|
||||
cert(manager.io/cluster-issuer": issue #275
|
||||
data:
|
||||
file.name: "a value"
|
||||
8
fixtures/annotation_missing_value.yaml
Normal file
8
fixtures/annotation_missing_value.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: some-values
|
||||
annotations:
|
||||
some.domain/some-key:
|
||||
data:
|
||||
file.name: "a value"
|
||||
8
fixtures/annotation_null_value.yaml
Normal file
8
fixtures/annotation_null_value.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: some-values
|
||||
annotations:
|
||||
some.domain/some-key: null
|
||||
data:
|
||||
file.name: "a value"
|
||||
8
fixtures/label_name_length.yaml
Normal file
8
fixtures/label_name_length.yaml
Normal 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"
|
||||
8
fixtures/label_namespace.yaml
Normal file
8
fixtures/label_namespace.yaml
Normal 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"
|
||||
8
fixtures/label_value_length.yaml
Normal file
8
fixtures/label_value_length.yaml
Normal 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"
|
||||
9
fixtures/metadata_extra.yaml
Normal file
9
fixtures/metadata_extra.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: some-values
|
||||
namespace: my-namespace
|
||||
annotation:
|
||||
flub: annotation should be annotations
|
||||
data:
|
||||
file.name: "a value"
|
||||
4
fixtures/metadata_missing.yaml
Normal file
4
fixtures/metadata_missing.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
data:
|
||||
file.name: "a value"
|
||||
6
fixtures/metadata_name_missing.yaml
Normal file
6
fixtures/metadata_name_missing.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
|
||||
data:
|
||||
file.name: "a value"
|
||||
6
fixtures/object_name-max_length.yaml
Normal file
6
fixtures/object_name-max_length.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: abcdefghijklmnopqrstuvwxyz-01234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
data:
|
||||
file.name: "a value"
|
||||
|
|
@ -22,6 +22,7 @@ type Config struct {
|
|||
RejectKinds map[string]struct{} `yaml:"reject" json:"reject"`
|
||||
SchemaLocations []string `yaml:"schemaLocations" json:"schemaLocations"`
|
||||
SkipKinds map[string]struct{} `yaml:"skip" json:"skip"`
|
||||
SkipMetadata bool `yaml:"skipMetadata" json:"skipMetadata"`
|
||||
SkipTLS bool `yaml:"insecureSkipTLSVerify" json:"insecureSkipTLSVerify"`
|
||||
Strict bool `yaml:"strict" json:"strict"`
|
||||
Summary bool `yaml:"summary" json:"summary"`
|
||||
|
|
@ -97,6 +98,7 @@ func FromFlags(progName string, args []string) (Config, string, error) {
|
|||
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, pretty, tap, text")
|
||||
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap and junit output)")
|
||||
flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
|
||||
flags.BoolVar(&c.SkipMetadata, "skip-metadata", false, "skip extra validations of metadata section")
|
||||
flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder")
|
||||
flags.BoolVar(&c.Help, "h", false, "show help information")
|
||||
flags.BoolVar(&c.Version, "v", false, "show version information")
|
||||
|
|
|
|||
35
pkg/registry/embeded.go
Normal file
35
pkg/registry/embeded.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed *.json
|
||||
var content embed.FS
|
||||
|
||||
type EmbeddedRegistry struct {
|
||||
debug bool
|
||||
strict bool
|
||||
}
|
||||
|
||||
// NewEmbeddedRegistry creates a new "registry", that will serve schemas from embedded resource
|
||||
func NewEmbeddedRegistry(debug bool, strict bool) *EmbeddedRegistry {
|
||||
return &EmbeddedRegistry{
|
||||
debug, strict,
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadSchema retrieves the schema from a file for the resource
|
||||
func (r EmbeddedRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, []byte, error) {
|
||||
var fileName string
|
||||
if r.strict {
|
||||
fileName = resourceKind + "-strict.json"
|
||||
} else {
|
||||
fileName = resourceKind + ".json"
|
||||
}
|
||||
bytes, err := content.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return resourceKind, nil, nil
|
||||
}
|
||||
return "embedded:" + resourceKind, bytes, nil
|
||||
}
|
||||
110
pkg/registry/metadata-strict.json
Normal file
110
pkg/registry/metadata-strict.json
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"$ref": "#/$defs/PREFIXED"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/$defs/NAME"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"$ref": "#/$defs/PREFIXED"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"finalizers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"generateName": {
|
||||
"$ref": "#/$defs/RFC-1123-prefix"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"$ref": "#/$defs/PREFIXED"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"$ref": "#/$defs/NAME"
|
||||
}
|
||||
}
|
||||
},
|
||||
"managedFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/$defs/RFC-1123"
|
||||
},
|
||||
"namespace": {
|
||||
"$ref": "#/$defs/RFC-1123"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"generateName"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apiVersion",
|
||||
"kind",
|
||||
"metadata"
|
||||
],
|
||||
"$defs": {
|
||||
"PREFIXED": {
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^(.{0,253}/)?.{1,63}$",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"pattern": "^([a-z0-9-]{1,63}(\\.[a-z0-9-]{1,63})*/)?[a-z0-9A-Z]+([_.-][a-z0-9A-Z]+)*$"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NAME": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 63,
|
||||
"pattern": "^[a-z0-9A-Z]+([_.-][a-z0-9A-Z]+)*$"
|
||||
},
|
||||
"RFC-1123": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 63,
|
||||
"pattern": "^[a-z0-9]+(-+[a-z0-9]+)*$"
|
||||
},
|
||||
"RFC-1123-prefix": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 58,
|
||||
"pattern": "^[a-z0-9]+[a-z0-9-]*$"
|
||||
}
|
||||
}
|
||||
}
|
||||
109
pkg/registry/metadata.json
Normal file
109
pkg/registry/metadata.json
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"$ref": "#/$defs/PREFIXED"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/$defs/NAME"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"$ref": "#/$defs/PREFIXED"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"finalizers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"generateName": {
|
||||
"$ref": "#/$defs/RFC-1123-prefix"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"$ref": "#/$defs/PREFIXED"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"$ref": "#/$defs/NAME"
|
||||
}
|
||||
}
|
||||
},
|
||||
"managedFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/$defs/RFC-1123"
|
||||
},
|
||||
"namespace": {
|
||||
"$ref": "#/$defs/RFC-1123"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"generateName"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apiVersion",
|
||||
"kind",
|
||||
"metadata"
|
||||
],
|
||||
"$defs": {
|
||||
"PREFIXED": {
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^(.{0,253}/)?.{1,63}$",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"pattern": "^([a-z0-9-]{1,63}(\\.[a-z0-9-]{1,63})*/)?[a-z0-9A-Z]+([_.-][a-z0-9A-Z]+)*$"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NAME": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 63,
|
||||
"pattern": "^[a-z0-9A-Z]+([_.-][a-z0-9A-Z]+)*$"
|
||||
},
|
||||
"RFC-1123": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 63,
|
||||
"pattern": "^[a-z0-9]+(-+[a-z0-9]+)*$"
|
||||
},
|
||||
"RFC-1123-prefix": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 58,
|
||||
"pattern": "^[a-z0-9]+[a-z0-9-]*$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ type Opts struct {
|
|||
Debug bool // Debug infos will be print here
|
||||
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
|
||||
SkipKinds map[string]struct{} // List of resource Kinds to ignore
|
||||
SkipMetadata bool // skip extra validation of metadata
|
||||
RejectKinds map[string]struct{} // List of resource Kinds to reject
|
||||
KubernetesVersion string // Kubernetes Version - has to match one in https://github.com/instrumenta/kubernetes-json-schema
|
||||
Strict bool // thros an error if resources contain undocumented fields
|
||||
|
|
@ -92,11 +93,17 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
|||
opts.RejectKinds = map[string]struct{}{}
|
||||
}
|
||||
|
||||
var metadataSchema *jsonschema.Schema = nil
|
||||
if !opts.SkipMetadata {
|
||||
metadataSchema, _ = downloadSchema([]registry.Registry{registry.NewEmbeddedRegistry(opts.Debug, opts.Strict)}, "metadata", "", "")
|
||||
}
|
||||
|
||||
return &v{
|
||||
opts: opts,
|
||||
schemaDownload: downloadSchema,
|
||||
schemaCache: cache.NewInMemoryCache(),
|
||||
regs: registries,
|
||||
metadataSchema: metadataSchema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +112,23 @@ type v struct {
|
|||
schemaCache cache.Cache
|
||||
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*jsonschema.Schema, error)
|
||||
regs []registry.Registry
|
||||
metadataSchema *jsonschema.Schema
|
||||
}
|
||||
|
||||
func validate(schema *jsonschema.Schema, r map[string]interface{}, validationErrors []ValidationError) ([]ValidationError, error) {
|
||||
err := schema.Validate(r)
|
||||
if err != nil {
|
||||
var e *jsonschema.ValidationError
|
||||
if errors.As(err, &e) {
|
||||
for _, ve := range e.Causes {
|
||||
validationErrors = append(validationErrors, ValidationError{
|
||||
Path: ve.InstanceLocation,
|
||||
Msg: ve.Message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return validationErrors, err
|
||||
}
|
||||
|
||||
// ValidateResource validates a single resource. This allows to validate
|
||||
|
|
@ -162,6 +186,12 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
return Result{Resource: res, Err: fmt.Errorf("prohibited resource kind %s", sig.Kind), Status: Error}
|
||||
}
|
||||
|
||||
validationErrors := []ValidationError{}
|
||||
var metaDataError error = nil
|
||||
if val.metadataSchema != nil {
|
||||
validationErrors, metaDataError = validate(val.metadataSchema, r, validationErrors)
|
||||
}
|
||||
|
||||
cached := false
|
||||
var schema *jsonschema.Schema
|
||||
|
||||
|
|
@ -183,27 +213,21 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
}
|
||||
}
|
||||
|
||||
status := Valid
|
||||
if schema == nil {
|
||||
if val.opts.IgnoreMissingSchemas {
|
||||
return Result{Resource: res, Err: nil, Status: Skipped}
|
||||
status = Skipped
|
||||
} else {
|
||||
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
|
||||
}
|
||||
} else {
|
||||
validationErrors, err = validate(schema, r, validationErrors)
|
||||
if err == nil {
|
||||
err = metaDataError
|
||||
}
|
||||
|
||||
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
|
||||
}
|
||||
|
||||
err = schema.Validate(r)
|
||||
if err != nil {
|
||||
validationErrors := []ValidationError{}
|
||||
var e *jsonschema.ValidationError
|
||||
if errors.As(err, &e) {
|
||||
for _, ve := range e.Causes {
|
||||
validationErrors = append(validationErrors, ValidationError{
|
||||
Path: ve.InstanceLocation,
|
||||
Msg: ve.Message,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if len(validationErrors) > 0 {
|
||||
return Result{
|
||||
Resource: res,
|
||||
Status: Invalid,
|
||||
|
|
@ -211,8 +235,7 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
|||
ValidationErrors: validationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return Result{Resource: res, Status: Valid}
|
||||
return Result{Resource: res, Status: status}
|
||||
}
|
||||
|
||||
// ValidateWithContext validates resources found in r
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ lastName: bar
|
|||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
SkipMetadata: true,
|
||||
RejectKinds: map[string]struct{}{},
|
||||
IgnoreMissingSchemas: testCase.ignoreMissingSchema,
|
||||
Strict: testCase.strict,
|
||||
|
|
@ -453,8 +454,9 @@ age: not a number
|
|||
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
SkipKinds: map[string]struct{}{},
|
||||
SkipMetadata: true,
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
schemaCache: nil,
|
||||
schemaDownload: downloadSchema,
|
||||
|
|
@ -502,8 +504,9 @@ firstName: foo
|
|||
|
||||
val := v{
|
||||
opts: Opts{
|
||||
SkipKinds: map[string]struct{}{},
|
||||
RejectKinds: map[string]struct{}{},
|
||||
SkipKinds: map[string]struct{}{},
|
||||
SkipMetadata: true,
|
||||
RejectKinds: map[string]struct{}{},
|
||||
},
|
||||
schemaCache: nil,
|
||||
schemaDownload: downloadSchema,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
|||
override schemas location search path (can be specified multiple times)
|
||||
-skip string
|
||||
comma-separated list of kinds to ignore
|
||||
-skip-metadata
|
||||
skip extra validations of metadata section
|
||||
-strict
|
||||
disallow additional properties not in schema or duplicated keys
|
||||
-summary
|
||||
|
|
|
|||
Loading…
Reference in a new issue