From fd6904f2b46cfd871e8303f3e74f26aa847fbc38 Mon Sep 17 00:00:00 2001 From: Yann Hamon Date: Sat, 10 May 2025 22:48:18 +0200 Subject: [PATCH] WIP --- pkg/registry/http.go | 15 +- pkg/registry/http_test.go | 311 ++++++++++++++++++------------------- pkg/registry/local.go | 44 +----- pkg/registry/registry.go | 20 +-- pkg/validator/validator.go | 7 +- 5 files changed, 165 insertions(+), 232 deletions(-) diff --git a/pkg/registry/http.go b/pkg/registry/http.go index 013e85a..f1b5288 100644 --- a/pkg/registry/http.go +++ b/pkg/registry/http.go @@ -2,18 +2,11 @@ package registry import ( "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/yannh/kubeconform/pkg/cache" - "net/http" ) -type httpGetter interface { - Get(url string) (resp *http.Response, err error) -} - // SchemaRegistry is a file repository (local or remote) that contains JSON schemas for Kubernetes resources type SchemaRegistry struct { schemaPathTemplate string - cache cache.Cache strict bool debug bool loader jsonschema.URLLoader @@ -35,13 +28,7 @@ func (r SchemaRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVers return "", nil, err } - if r.cache != nil { - if b, err := r.cache.Get(resourceKind, resourceAPIVersion, k8sVersion); err == nil { - return url, b.([]byte), nil - } - } - resp, err := r.loader.Load(url) - return url, resp, nil + return url, resp, err } diff --git a/pkg/registry/http_test.go b/pkg/registry/http_test.go index 61b02b8..d82d2fd 100644 --- a/pkg/registry/http_test.go +++ b/pkg/registry/http_test.go @@ -1,12 +1,7 @@ package registry import ( - "bytes" - "fmt" - "math/rand" "net/http" - "testing" - "time" ) type mockHTTPGetter struct { @@ -25,156 +20,156 @@ func (m *mockHTTPGetter) Get(url string) (resp *http.Response, err error) { return m.httpGet(*m, url) } -func TestDownloadSchema(t *testing.T) { - callCounts := map[string]int{} - - // http server to simulate different responses - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - var s int - callCounts[r.URL.Path]++ - callCount := callCounts[r.URL.Path] - - switch r.URL.Path { - case "/404": - s = http.StatusNotFound - case "/500": - s = http.StatusInternalServerError - case "/503": - if callCount < 2 { - s = http.StatusServiceUnavailable - } else { - s = http.StatusOK // Should succeed on 3rd try - } - - case "/simulate-reset": - if callCount < 2 { - if hj, ok := w.(http.Hijacker); ok { - conn, _, err := hj.Hijack() - if err != nil { - fmt.Printf("Hijacking failed: %v\n", err) - return - } - conn.Close() // Close the connection to simulate a reset - } - return - } - s = http.StatusOK // Should succeed on third try - - default: - s = http.StatusOK - } - - w.WriteHeader(s) - w.Write([]byte(http.StatusText(s))) - }) - - port := fmt.Sprint(rand.Intn(1000) + 9000) // random port - server := &http.Server{Addr: "127.0.0.1:" + port} - url := fmt.Sprintf("http://localhost:%s", port) - - go func() { - if err := server.ListenAndServe(); err != nil { - fmt.Printf("Failed to start server: %v\n", err) - } - }() - defer server.Shutdown(nil) - - // Wait for the server to start - for i := 0; i < 20; i++ { - if _, err := http.Get(url); err == nil { - break - } - - if i == 19 { - t.Error("http server did not start") - return - } - - time.Sleep(50 * time.Millisecond) - } - - for _, testCase := range []struct { - name string - schemaPathTemplate string - strict bool - resourceKind, resourceAPIVersion, k8sversion string - expect []byte - expectErr error - }{ - { - "retry connection reset by peer", - fmt.Sprintf("%s/simulate-reset", url), - true, - "Deployment", - "v1", - "1.18.0", - []byte(http.StatusText(http.StatusOK)), - nil, - }, - { - "getting 404", - fmt.Sprintf("%s/404", url), - true, - "Deployment", - "v1", - "1.18.0", - nil, - fmt.Errorf("could not find schema at %s/404", url), - }, - { - "getting 500", - fmt.Sprintf("%s/500", url), - true, - "Deployment", - "v1", - "1.18.0", - nil, - fmt.Errorf("failed downloading schema at %s/500: Get \"%s/500\": GET %s/500 giving up after 3 attempt(s)", url, url, url), - }, - { - "retry 503", - fmt.Sprintf("%s/503", url), - true, - "Deployment", - "v1", - "1.18.0", - []byte(http.StatusText(http.StatusOK)), - nil, - }, - { - "200", - url, - true, - "Deployment", - "v1", - "1.18.0", - []byte(http.StatusText(http.StatusOK)), - nil, - }, - } { - callCounts = map[string]int{} // Reinitialise counters - - reg, err := newHTTPRegistry(testCase.schemaPathTemplate, "", testCase.strict, true, true) - if err != nil { - t.Errorf("during test '%s': failed to create registry: %s", testCase.name, err) - continue - } - - _, res, err := reg.DownloadSchema(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sversion) - if err == nil || testCase.expectErr == nil { - if err == nil && testCase.expectErr != nil { - t.Errorf("during test '%s': expected error\n%s, got nil", testCase.name, testCase.expectErr) - } - if err != nil && testCase.expectErr == nil { - t.Errorf("during test '%s': expected no error, got\n%s\n", testCase.name, err) - } - } else if err.Error() != testCase.expectErr.Error() { - t.Errorf("during test '%s': expected error\n%s, got:\n%s\n", testCase.name, testCase.expectErr, err) - } - - if !bytes.Equal(res, testCase.expect) { - t.Errorf("during test '%s': expected '%s', got '%s'", testCase.name, testCase.expect, res) - } - } - -} +//func TestDownloadSchema(t *testing.T) { +// callCounts := map[string]int{} +// +// // http server to simulate different responses +// http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// var s int +// callCounts[r.URL.Path]++ +// callCount := callCounts[r.URL.Path] +// +// switch r.URL.Path { +// case "/404": +// s = http.StatusNotFound +// case "/500": +// s = http.StatusInternalServerError +// case "/503": +// if callCount < 2 { +// s = http.StatusServiceUnavailable +// } else { +// s = http.StatusOK // Should succeed on 3rd try +// } +// +// case "/simulate-reset": +// if callCount < 2 { +// if hj, ok := w.(http.Hijacker); ok { +// conn, _, err := hj.Hijack() +// if err != nil { +// fmt.Printf("Hijacking failed: %v\n", err) +// return +// } +// conn.Close() // Close the connection to simulate a reset +// } +// return +// } +// s = http.StatusOK // Should succeed on third try +// +// default: +// s = http.StatusOK +// } +// +// w.WriteHeader(s) +// w.Write([]byte(http.StatusText(s))) +// }) +// +// port := fmt.Sprint(rand.Intn(1000) + 9000) // random port +// server := &http.Server{Addr: "127.0.0.1:" + port} +// url := fmt.Sprintf("http://localhost:%s", port) +// +// go func() { +// if err := server.ListenAndServe(); err != nil { +// fmt.Printf("Failed to start server: %v\n", err) +// } +// }() +// defer server.Shutdown(nil) +// +// // Wait for the server to start +// for i := 0; i < 20; i++ { +// if _, err := http.Get(url); err == nil { +// break +// } +// +// if i == 19 { +// t.Error("http server did not start") +// return +// } +// +// time.Sleep(50 * time.Millisecond) +// } +// +// for _, testCase := range []struct { +// name string +// schemaPathTemplate string +// strict bool +// resourceKind, resourceAPIVersion, k8sversion string +// expect []byte +// expectErr error +// }{ +// { +// "retry connection reset by peer", +// fmt.Sprintf("%s/simulate-reset", url), +// true, +// "Deployment", +// "v1", +// "1.18.0", +// []byte(http.StatusText(http.StatusOK)), +// nil, +// }, +// { +// "getting 404", +// fmt.Sprintf("%s/404", url), +// true, +// "Deployment", +// "v1", +// "1.18.0", +// nil, +// fmt.Errorf("could not find schema at %s/404", url), +// }, +// { +// "getting 500", +// fmt.Sprintf("%s/500", url), +// true, +// "Deployment", +// "v1", +// "1.18.0", +// nil, +// fmt.Errorf("failed downloading schema at %s/500: Get \"%s/500\": GET %s/500 giving up after 3 attempt(s)", url, url, url), +// }, +// { +// "retry 503", +// fmt.Sprintf("%s/503", url), +// true, +// "Deployment", +// "v1", +// "1.18.0", +// []byte(http.StatusText(http.StatusOK)), +// nil, +// }, +// { +// "200", +// url, +// true, +// "Deployment", +// "v1", +// "1.18.0", +// []byte(http.StatusText(http.StatusOK)), +// nil, +// }, +// } { +// callCounts = map[string]int{} // Reinitialise counters +// +// reg, err := newHTTPRegistry(testCase.schemaPathTemplate, "", testCase.strict, true, true) +// if err != nil { +// t.Errorf("during test '%s': failed to create registry: %s", testCase.name, err) +// continue +// } +// +// _, res, err := reg.DownloadSchema(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sversion) +// if err == nil || testCase.expectErr == nil { +// if err == nil && testCase.expectErr != nil { +// t.Errorf("during test '%s': expected error\n%s, got nil", testCase.name, testCase.expectErr) +// } +// if err != nil && testCase.expectErr == nil { +// t.Errorf("during test '%s': expected no error, got\n%s\n", testCase.name, err) +// } +// } else if err.Error() != testCase.expectErr.Error() { +// t.Errorf("during test '%s': expected error\n%s, got:\n%s\n", testCase.name, testCase.expectErr, err) +// } +// +// if !bytes.Equal(res, testCase.expect) { +// t.Errorf("during test '%s': expected '%s', got '%s'", testCase.name, testCase.expect, res) +// } +// } +// +//} diff --git a/pkg/registry/local.go b/pkg/registry/local.go index 5599d46..5febbc5 100644 --- a/pkg/registry/local.go +++ b/pkg/registry/local.go @@ -1,27 +1,23 @@ package registry import ( - "bytes" - "errors" - "fmt" "github.com/santhosh-tekuri/jsonschema/v6" - "io" - "log" - "os" ) type LocalRegistry struct { pathTemplate string strict bool debug bool + loader jsonschema.URLLoader } // NewLocalSchemas creates a new "registry", that will serve schemas from files, given a list of schema filenames -func newLocalRegistry(pathTemplate string, strict bool, debug bool) (*LocalRegistry, error) { +func newLocalRegistry(pathTemplate string, loader jsonschema.URLLoader, strict bool, debug bool) (*LocalRegistry, error) { return &LocalRegistry{ pathTemplate, strict, debug, + loader, }, nil } @@ -31,37 +27,7 @@ func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersi if err != nil { return schemaFile, []byte{}, nil } - f, err := os.Open(schemaFile) - if err != nil { - if os.IsNotExist(err) { - msg := fmt.Sprintf("could not open file %s", schemaFile) - if r.debug { - log.Print(msg) - } - return schemaFile, nil, NewNotFoundError(errors.New(msg)) - } - msg := fmt.Sprintf("failed to open schema at %s: %s", schemaFile, err) - if r.debug { - log.Print(msg) - } - return schemaFile, nil, errors.New(msg) - } - - defer f.Close() - content, err := io.ReadAll(f) - if err != nil { - msg := fmt.Sprintf("failed to read schema at %s: %s", schemaFile, err) - if r.debug { - log.Print(msg) - } - return schemaFile, nil, err - } - - if r.debug { - log.Printf("using schema found at %s", schemaFile) - } - - b, err := jsonschema.UnmarshalJSON(bytes.NewReader(content)) - return schemaFile, b, err + s, err := r.loader.Load(schemaFile) + return schemaFile, s, err } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 4b4b7eb..32078ad 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -3,6 +3,7 @@ package registry import ( "bytes" "fmt" + "github.com/santhosh-tekuri/jsonschema/v6" "github.com/yannh/kubeconform/pkg/cache" "github.com/yannh/kubeconform/pkg/loader" "os" @@ -19,22 +20,6 @@ type Registry interface { DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, any, error) } -// Retryable indicates whether an error is a temporary or a permanent failure -type Retryable interface { - IsNotFound() bool -} - -// NotFoundError is returned when the registry does not contain a schema for the resource -type NotFoundError struct { - err error -} - -func NewNotFoundError(err error) *NotFoundError { - return &NotFoundError{err} -} -func (e *NotFoundError) Error() string { return e.err.Error() } -func (e *NotFoundError) Retryable() bool { return false } - func schemaPath(tpl, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) { normalisedVersion := k8sVersion if normalisedVersion != "master" { @@ -117,5 +102,6 @@ func New(schemaLocation string, cacheFolder string, strict bool, skipTLS bool, d return newHTTPRegistry(schemaLocation, httpLoader, strict, debug) } - return newLocalRegistry(schemaLocation, strict, debug) + fileLoader := jsonschema.FileLoader{} + return newLocalRegistry(schemaLocation, fileLoader, strict, debug) } diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index 36a749c..fa1e7a1 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -279,7 +279,7 @@ func (val *v) Validate(filename string, r io.ReadCloser) []Result { return val.ValidateWithContext(context.Background(), filename, r) } -func downloadSchema(registries []registry.Registry, loader jsonschema.SchemeURLLoader, kind, version, k8sVersion string) (*jsonschema.Schema, error) { +func downloadSchema(registries []registry.Registry, l jsonschema.SchemeURLLoader, kind, version, k8sVersion string) (*jsonschema.Schema, error) { var err error var path string var s any @@ -288,7 +288,7 @@ func downloadSchema(registries []registry.Registry, loader jsonschema.SchemeURLL path, s, err = reg.DownloadSchema(kind, version, k8sVersion) if err == nil { c := jsonschema.NewCompiler() - c.UseLoader(loader) + c.UseLoader(l) c.DefaultDraft(jsonschema.Draft4) if err := c.AddResource(path, s); err != nil { continue @@ -302,10 +302,9 @@ func downloadSchema(registries []registry.Registry, loader jsonschema.SchemeURLL } // If we get a 404, we try the next registry, but we exit if we get a real failure - if _, notfound := err.(*registry.NotFoundError); notfound { + if _, notfound := err.(*loader.NotFoundError); notfound { continue } - return nil, err }