mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-21 10:57:01 +00:00
cache schemas downloaded over HTTP
This commit is contained in:
parent
1a76217195
commit
18927ddf75
10 changed files with 144 additions and 52 deletions
|
|
@ -49,6 +49,10 @@ configuration errors.
|
||||||
```
|
```
|
||||||
$ ./bin/kubeconform -h
|
$ ./bin/kubeconform -h
|
||||||
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
|
||||||
|
-cache string
|
||||||
|
cache schemas downloaded via HTTP to this folder
|
||||||
|
-cpu-prof string
|
||||||
|
debug - log CPU profiling to file
|
||||||
-exit-on-error
|
-exit-on-error
|
||||||
immediately stop execution when the first error is encountered
|
immediately stop execution when the first error is encountered
|
||||||
-h show help information
|
-h show help information
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ func realMain() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
|
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
|
||||||
|
Cache: cfg.Cache,
|
||||||
SkipTLS: cfg.SkipTLS,
|
SkipTLS: cfg.SkipTLS,
|
||||||
SkipKinds: cfg.SkipKinds,
|
SkipKinds: cfg.SkipKinds,
|
||||||
RejectKinds: cfg.RejectKinds,
|
RejectKinds: cfg.RejectKinds,
|
||||||
|
|
|
||||||
6
pkg/cache/cache.go
vendored
Normal file
6
pkg/cache/cache.go
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
type Cache interface {
|
||||||
|
Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error)
|
||||||
|
Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error
|
||||||
|
}
|
||||||
49
pkg/cache/inmemory.go
vendored
Normal file
49
pkg/cache/inmemory.go
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchemaCache is a cache for downloaded schemas, so each file is only retrieved once
|
||||||
|
// It is different from pkg/registry/http_cache.go in that:
|
||||||
|
// - This cache caches the parsed Schemas
|
||||||
|
type inMemory struct {
|
||||||
|
sync.RWMutex
|
||||||
|
schemas map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new cache for downloaded schemas
|
||||||
|
func NewInMemoryCache() Cache {
|
||||||
|
return &inMemory{
|
||||||
|
schemas: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func key(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||||
|
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the JSON schema given a resource signature
|
||||||
|
func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
|
||||||
|
k := key(resourceKind, resourceAPIVersion, k8sVersion)
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
schema, ok := c.schemas[k]
|
||||||
|
|
||||||
|
if ok == false {
|
||||||
|
return nil, fmt.Errorf("schema not found in in-memory cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds a JSON schema to the schema cache
|
||||||
|
func (c *inMemory) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
|
||||||
|
k := key(resourceKind, resourceAPIVersion, k8sVersion)
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
c.schemas[k] = schema
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
pkg/cache/ondisk.go
vendored
Normal file
48
pkg/cache/ondisk.go
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type onDisk struct {
|
||||||
|
sync.RWMutex
|
||||||
|
folder string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new cache for downloaded schemas
|
||||||
|
func NewOnDiskCache(cache string) Cache {
|
||||||
|
return &onDisk{
|
||||||
|
folder: cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
|
||||||
|
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
|
||||||
|
return path.Join(folder, hex.EncodeToString(hash[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the JSON schema given a resource signature
|
||||||
|
func (c *onDisk) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
f, err := os.Open(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.ReadAll(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds a JSON schema to the schema cache
|
||||||
|
func (c *onDisk) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
return ioutil.WriteFile(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion), schema.([]byte), 0644)
|
||||||
|
}
|
||||||
42
pkg/cache/schemacache.go
vendored
42
pkg/cache/schemacache.go
vendored
|
|
@ -1,42 +0,0 @@
|
||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/xeipuuv/gojsonschema"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SchemaCache is a cache for downloaded schemas, so each file is only retrieved once
|
|
||||||
type SchemaCache struct {
|
|
||||||
sync.RWMutex
|
|
||||||
schemas map[string]*gojsonschema.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new cache for downloaded schemas
|
|
||||||
func New() *SchemaCache {
|
|
||||||
return &SchemaCache{
|
|
||||||
schemas: map[string]*gojsonschema.Schema{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key computes a key for a specific JSON schema from its Kind, the resource API Version, and the
|
|
||||||
// Kubernetes version
|
|
||||||
func Key(resourceKind, resourceAPIVersion, k8sVersion string) string {
|
|
||||||
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves the JSON schema given a resource signature
|
|
||||||
func (c *SchemaCache) Get(key string) (*gojsonschema.Schema, bool) {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
schema, ok := c.schemas[key]
|
|
||||||
return schema, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set adds a JSON schema to the schema cache
|
|
||||||
func (c *SchemaCache) Set(key string, schema *gojsonschema.Schema) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
c.schemas[key] = schema
|
|
||||||
}
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Cache string
|
||||||
CPUProfileFile string
|
CPUProfileFile string
|
||||||
ExitOnError bool
|
ExitOnError bool
|
||||||
Files []string
|
Files []string
|
||||||
|
|
@ -75,6 +76,7 @@ func FromFlags(progName string, args []string) (Config, string, error) {
|
||||||
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, tap, text")
|
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, tap, text")
|
||||||
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap output)")
|
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap 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.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
|
||||||
|
flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder")
|
||||||
flags.StringVar(&c.CPUProfileFile, "cpu-prof", "", "debug - log CPU profiling to file")
|
flags.StringVar(&c.CPUProfileFile, "cpu-prof", "", "debug - log CPU profiling to file")
|
||||||
flags.BoolVar(&c.Help, "h", false, "show help information")
|
flags.BoolVar(&c.Help, "h", false, "show help information")
|
||||||
flags.Usage = func() {
|
flags.Usage = func() {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yannh/kubeconform/pkg/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
type httpGetter interface {
|
type httpGetter interface {
|
||||||
|
|
@ -16,10 +18,11 @@ type httpGetter interface {
|
||||||
type SchemaRegistry struct {
|
type SchemaRegistry struct {
|
||||||
c httpGetter
|
c httpGetter
|
||||||
schemaPathTemplate string
|
schemaPathTemplate string
|
||||||
|
cache cache.Cache
|
||||||
strict bool
|
strict bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPRegistry(schemaPathTemplate string, strict bool, skipTLS bool) *SchemaRegistry {
|
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool) *SchemaRegistry {
|
||||||
reghttp := &http.Transport{
|
reghttp := &http.Transport{
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
IdleConnTimeout: 3 * time.Second,
|
IdleConnTimeout: 3 * time.Second,
|
||||||
|
|
@ -30,9 +33,15 @@ func newHTTPRegistry(schemaPathTemplate string, strict bool, skipTLS bool) *Sche
|
||||||
reghttp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
reghttp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var filecache cache.Cache = nil
|
||||||
|
if cacheFolder != "" {
|
||||||
|
filecache = cache.NewOnDiskCache(cacheFolder)
|
||||||
|
}
|
||||||
|
|
||||||
return &SchemaRegistry{
|
return &SchemaRegistry{
|
||||||
c: &http.Client{Transport: reghttp},
|
c: &http.Client{Transport: reghttp},
|
||||||
schemaPathTemplate: schemaPathTemplate,
|
schemaPathTemplate: schemaPathTemplate,
|
||||||
|
cache: filecache,
|
||||||
strict: strict,
|
strict: strict,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +53,12 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.cache != nil {
|
||||||
|
if b, err := r.cache.Get(resourceKind, resourceAPIVersion, k8sVersion); err == nil {
|
||||||
|
return b.([]byte), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := r.c.Get(url)
|
resp, err := r.c.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
||||||
|
|
@ -63,5 +78,11 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers
|
||||||
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.cache != nil {
|
||||||
|
if err := r.cache.Set(resourceKind, resourceAPIVersion, k8sVersion, body); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed writing schema to cache: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(schemaLocation string, strict bool, skipTLS bool) (Registry, error) {
|
func New(schemaLocation string, cache string, strict bool, skipTLS bool) (Registry, error) {
|
||||||
if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of kubernetesjsonschema.dev
|
if !strings.HasSuffix(schemaLocation, "json") { // If we dont specify a full templated path, we assume the paths of kubernetesjsonschema.dev
|
||||||
schemaLocation += "/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
|
schemaLocation += "/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +90,7 @@ func New(schemaLocation string, strict bool, skipTLS bool) (Registry, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(schemaLocation, "http") {
|
if strings.HasPrefix(schemaLocation, "http") {
|
||||||
return newHTTPRegistry(schemaLocation, strict, skipTLS), nil
|
return newHTTPRegistry(schemaLocation, cache, strict, skipTLS), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return newLocalRegistry(schemaLocation, strict), nil
|
return newLocalRegistry(schemaLocation, strict), nil
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ type Validator interface {
|
||||||
|
|
||||||
// Opts contains a set of options for the validator.
|
// Opts contains a set of options for the validator.
|
||||||
type Opts struct {
|
type Opts struct {
|
||||||
|
Cache string // Cache schemas downloaded via HTTP to this folder
|
||||||
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
|
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
|
||||||
SkipKinds map[string]struct{} // List of resource Kinds to ignore
|
SkipKinds map[string]struct{} // List of resource Kinds to ignore
|
||||||
RejectKinds map[string]struct{} // List of resource Kinds to reject
|
RejectKinds map[string]struct{} // List of resource Kinds to reject
|
||||||
|
|
@ -59,7 +60,7 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
||||||
|
|
||||||
registries := []registry.Registry{}
|
registries := []registry.Registry{}
|
||||||
for _, schemaLocation := range schemaLocations {
|
for _, schemaLocation := range schemaLocations {
|
||||||
reg, err := registry.New(schemaLocation, opts.Strict, opts.SkipTLS)
|
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -80,14 +81,14 @@ func New(schemaLocations []string, opts Opts) (Validator, error) {
|
||||||
return &v{
|
return &v{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
schemaDownload: downloadSchema,
|
schemaDownload: downloadSchema,
|
||||||
schemaCache: cache.New(),
|
schemaCache: cache.NewInMemoryCache(),
|
||||||
regs: registries,
|
regs: registries,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type v struct {
|
type v struct {
|
||||||
opts Opts
|
opts Opts
|
||||||
schemaCache *cache.SchemaCache
|
schemaCache cache.Cache
|
||||||
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error)
|
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*gojsonschema.Schema, error)
|
||||||
regs []registry.Registry
|
regs []registry.Registry
|
||||||
}
|
}
|
||||||
|
|
@ -133,11 +134,13 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
||||||
|
|
||||||
cached := false
|
cached := false
|
||||||
var schema *gojsonschema.Schema
|
var schema *gojsonschema.Schema
|
||||||
cacheKey := ""
|
|
||||||
|
|
||||||
if val.schemaCache != nil {
|
if val.schemaCache != nil {
|
||||||
cacheKey = cache.Key(sig.Kind, sig.Version, val.opts.KubernetesVersion)
|
s, err := val.schemaCache.Get(sig.Kind, sig.Version, val.opts.KubernetesVersion)
|
||||||
schema, cached = val.schemaCache.Get(cacheKey)
|
if err == nil {
|
||||||
|
cached = true
|
||||||
|
schema = s.(*gojsonschema.Schema)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cached {
|
if !cached {
|
||||||
|
|
@ -146,7 +149,7 @@ func (val *v) ValidateResource(res resource.Resource) Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
if val.schemaCache != nil {
|
if val.schemaCache != nil {
|
||||||
val.schemaCache.Set(cacheKey, schema)
|
val.schemaCache.Set(sig.Kind, sig.Version, val.opts.KubernetesVersion, schema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue