Skip to content

Commit c959d79

Browse files
authored
Merge pull request #24 from yannh/fs-cache
Cache schemas downloaded over HTTP
2 parents 1a76217 + 128fcf9 commit c959d79

File tree

12 files changed

+172
-57
lines changed

12 files changed

+172
-57
lines changed

Readme.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ It is inspired by, contains code from and is designed to stay close to
1313
* **high performance**: will validate & download manifests over multiple routines, caching
1414
downloaded files in memory
1515
* configurable list of **remote, or local schemas locations**, enabling validating Kubernetes
16-
custom resources (CRDs)
16+
custom resources (CRDs) and offline validation capabilities.
1717

1818
### A small overview of Kubernetes manifest validation
1919

@@ -49,6 +49,10 @@ configuration errors.
4949
```
5050
$ ./bin/kubeconform -h
5151
Usage: ./bin/kubeconform [OPTION]... [FILE OR FOLDER]...
52+
-cache string
53+
cache schemas downloaded via HTTP to this folder
54+
-cpu-prof string
55+
debug - log CPU profiling to file
5256
-exit-on-error
5357
immediately stop execution when the first error is encountered
5458
-h show help information

acceptance.bats

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,17 @@
202202
[ "$status" -eq 0 ]
203203
[ "$output" = "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0" ]
204204
}
205+
206+
@test "Pass when parsing a valid Kubernetes config YAML file and store cache" {
207+
run mkdir cache
208+
run bin/kubeconform -cache cache -summary fixtures/valid.yaml
209+
[ "$status" -eq 0 ]
210+
[ "$output" = "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0" ]
211+
[ "`ls cache/ | wc -l`" -eq 1 ]
212+
}
213+
214+
@test "Fail when cache folder does not exist" {
215+
run bin/kubeconform -cache cache_does_not_exist -summary fixtures/valid.yaml
216+
[ "$status" -eq 1 ]
217+
[ "$output" = "failed opening cache folder cache_does_not_exist: stat cache_does_not_exist: no such file or directory" ]
218+
}

cmd/kubeconform/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func realMain() int {
8282
}
8383

8484
v, err := validator.New(cfg.SchemaLocations, validator.Opts{
85+
Cache: cfg.Cache,
8586
SkipTLS: cfg.SkipTLS,
8687
SkipKinds: cfg.SkipKinds,
8788
RejectKinds: cfg.RejectKinds,

pkg/cache/cache.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package cache
2+
3+
type Cache interface {
4+
Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error)
5+
Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error
6+
}

pkg/cache/inmemory.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package cache
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
)
7+
8+
// SchemaCache is a cache for downloaded schemas, so each file is only retrieved once
9+
// It is different from pkg/registry/http_cache.go in that:
10+
// - This cache caches the parsed Schemas
11+
type inMemory struct {
12+
sync.RWMutex
13+
schemas map[string]interface{}
14+
}
15+
16+
// New creates a new cache for downloaded schemas
17+
func NewInMemoryCache() Cache {
18+
return &inMemory{
19+
schemas: map[string]interface{}{},
20+
}
21+
}
22+
23+
func key(resourceKind, resourceAPIVersion, k8sVersion string) string {
24+
return fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)
25+
}
26+
27+
// Get retrieves the JSON schema given a resource signature
28+
func (c *inMemory) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
29+
k := key(resourceKind, resourceAPIVersion, k8sVersion)
30+
c.RLock()
31+
defer c.RUnlock()
32+
schema, ok := c.schemas[k]
33+
34+
if ok == false {
35+
return nil, fmt.Errorf("schema not found in in-memory cache")
36+
}
37+
38+
return schema, nil
39+
}
40+
41+
// Set adds a JSON schema to the schema cache
42+
func (c *inMemory) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
43+
k := key(resourceKind, resourceAPIVersion, k8sVersion)
44+
c.Lock()
45+
defer c.Unlock()
46+
c.schemas[k] = schema
47+
48+
return nil
49+
}

pkg/cache/ondisk.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cache
2+
3+
import (
4+
"crypto/md5"
5+
"encoding/hex"
6+
"fmt"
7+
"io/ioutil"
8+
"os"
9+
"path"
10+
"sync"
11+
)
12+
13+
type onDisk struct {
14+
sync.RWMutex
15+
folder string
16+
}
17+
18+
// New creates a new cache for downloaded schemas
19+
func NewOnDiskCache(cache string) Cache {
20+
return &onDisk{
21+
folder: cache,
22+
}
23+
}
24+
25+
func cachePath(folder, resourceKind, resourceAPIVersion, k8sVersion string) string {
26+
hash := md5.Sum([]byte(fmt.Sprintf("%s-%s-%s", resourceKind, resourceAPIVersion, k8sVersion)))
27+
return path.Join(folder, hex.EncodeToString(hash[:]))
28+
}
29+
30+
// Get retrieves the JSON schema given a resource signature
31+
func (c *onDisk) Get(resourceKind, resourceAPIVersion, k8sVersion string) (interface{}, error) {
32+
c.RLock()
33+
defer c.RUnlock()
34+
35+
f, err := os.Open(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion))
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
return ioutil.ReadAll(f)
41+
}
42+
43+
// Set adds a JSON schema to the schema cache
44+
func (c *onDisk) Set(resourceKind, resourceAPIVersion, k8sVersion string, schema interface{}) error {
45+
c.Lock()
46+
defer c.Unlock()
47+
return ioutil.WriteFile(cachePath(c.folder, resourceKind, resourceAPIVersion, k8sVersion), schema.([]byte), 0644)
48+
}

pkg/cache/schemacache.go

Lines changed: 0 additions & 42 deletions
This file was deleted.

pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
)
1010

1111
type Config struct {
12+
Cache string
1213
CPUProfileFile string
1314
ExitOnError bool
1415
Files []string
@@ -75,6 +76,7 @@ func FromFlags(progName string, args []string) (Config, string, error) {
7576
flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, tap, text")
7677
flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap output)")
7778
flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure")
79+
flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder")
7880
flags.StringVar(&c.CPUProfileFile, "cpu-prof", "", "debug - log CPU profiling to file")
7981
flags.BoolVar(&c.Help, "h", false, "show help information")
8082
flags.Usage = func() {

pkg/registry/http.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import (
55
"fmt"
66
"io/ioutil"
77
"net/http"
8+
"os"
89
"time"
10+
11+
"github.com/yannh/kubeconform/pkg/cache"
912
)
1013

1114
type httpGetter interface {
@@ -16,10 +19,11 @@ type httpGetter interface {
1619
type SchemaRegistry struct {
1720
c httpGetter
1821
schemaPathTemplate string
22+
cache cache.Cache
1923
strict bool
2024
}
2125

22-
func newHTTPRegistry(schemaPathTemplate string, strict bool, skipTLS bool) *SchemaRegistry {
26+
func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool, skipTLS bool) (*SchemaRegistry, error) {
2327
reghttp := &http.Transport{
2428
MaxIdleConns: 100,
2529
IdleConnTimeout: 3 * time.Second,
@@ -30,11 +34,25 @@ func newHTTPRegistry(schemaPathTemplate string, strict bool, skipTLS bool) *Sche
3034
reghttp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
3135
}
3236

37+
var filecache cache.Cache = nil
38+
if cacheFolder != "" {
39+
fi, err := os.Stat(cacheFolder)
40+
if err != nil {
41+
return nil, fmt.Errorf("failed opening cache folder %s: %s", cacheFolder, err)
42+
}
43+
if !fi.IsDir() {
44+
return nil, fmt.Errorf("cache folder %s is not a directory", err)
45+
}
46+
47+
filecache = cache.NewOnDiskCache(cacheFolder)
48+
}
49+
3350
return &SchemaRegistry{
3451
c: &http.Client{Transport: reghttp},
3552
schemaPathTemplate: schemaPathTemplate,
53+
cache: filecache,
3654
strict: strict,
37-
}
55+
}, nil
3856
}
3957

4058
// DownloadSchema downloads the schema for a particular resource from an HTTP server
@@ -44,6 +62,12 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers
4462
return nil, err
4563
}
4664

65+
if r.cache != nil {
66+
if b, err := r.cache.Get(resourceKind, resourceAPIVersion, k8sVersion); err == nil {
67+
return b.([]byte), nil
68+
}
69+
}
70+
4771
resp, err := r.c.Get(url)
4872
if err != nil {
4973
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
@@ -63,5 +87,11 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers
6387
return nil, fmt.Errorf("failed downloading schema at %s: %s", url, err)
6488
}
6589

90+
if r.cache != nil {
91+
if err := r.cache.Set(resourceKind, resourceAPIVersion, k8sVersion, body); err != nil {
92+
return nil, fmt.Errorf("failed writing schema to cache: %s", err)
93+
}
94+
}
95+
6696
return body, nil
6797
}

pkg/registry/local.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ type LocalRegistry struct {
1212
}
1313

1414
// NewLocalSchemas creates a new "registry", that will serve schemas from files, given a list of schema filenames
15-
func newLocalRegistry(pathTemplate string, strict bool) *LocalRegistry {
15+
func newLocalRegistry(pathTemplate string, strict bool) (*LocalRegistry, error) {
1616
return &LocalRegistry{
1717
pathTemplate,
1818
strict,
19-
}
19+
}, nil
2020
}
2121

2222
// DownloadSchema retrieves the schema from a file for the resource

0 commit comments

Comments
 (0)