diff --git a/cmd/cup/ctl.go b/cmd/cup/ctl.go index ec6726b..e075920 100644 --- a/cmd/cup/ctl.go +++ b/cmd/cup/ctl.go @@ -14,6 +14,7 @@ import ( "go.flipt.io/cup/pkg/api/core" "go.flipt.io/cup/pkg/encoding" + "golang.org/x/exp/slog" ) func definitions(cfg config, client *http.Client) error { @@ -149,7 +150,7 @@ func apply(cfg config, client *http.Client, source string) (err error) { return err } - defs, err := getDefintions(cfg, client) + defs, err := getDefintionsByAPIVersionKind(cfg, client) if err != nil { return err } @@ -186,6 +187,13 @@ func apply(cfg config, client *http.Client, source string) (err error) { }() if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading unexpected response body: %w", err) + } + + slog.Error("Applying resource", "response", string(body)) + return fmt.Errorf("unexpected status: %q", resp.Status) } @@ -260,6 +268,22 @@ func getGVK(cfg config, client *http.Client, typ string) (group, version, kind s return } +func getDefintionsByAPIVersionKind(cfg config, client *http.Client) (map[string]*core.ResourceDefinition, error) { + m := map[string]*core.ResourceDefinition{} + defs, err := getDefintions(cfg, client) + if err != nil { + return nil, err + } + + for _, def := range defs { + for version := range def.Spec.Versions { + m[path.Join(def.Spec.Group, version, def.Names.Kind)] = def + } + } + + return m, nil +} + func getDefintions(cfg config, client *http.Client) (map[string]*core.ResourceDefinition, error) { req, err := http.NewRequest(http.MethodGet, cfg.Address()+"/apis", nil) if err != nil { diff --git a/cmd/cupd/serve.go b/cmd/cupd/serve.go index 05c18bf..d2234f9 100644 --- a/cmd/cupd/serve.go +++ b/cmd/cupd/serve.go @@ -106,9 +106,11 @@ func serve(ctx *cli.Context) error { return fmt.Errorf("controller type not supported: %q", controllerConf.Type) } - slog.Debug("Registering Controller", "kind", typ) + if err := srv.Register(controller, def); err != nil { + return fmt.Errorf("registering resource %q: %w", typ, err) + } - srv.Register(controller, def) + slog.Debug("Registered Resource Controller", "kind", typ, "controller", binding.Controller) } slog.Info("Listening...", "address", cfg.API.Address) diff --git a/go.mod b/go.mod index c99a9c3..0dc0c0e 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,9 @@ require ( github.com/sergi/go-diff v1.1.0 // indirect github.com/skeema/knownhosts v1.1.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.10.0 // indirect golang.org/x/mod v0.11.0 // indirect diff --git a/go.sum b/go.sum index 14f6ad4..7d58aee 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,7 @@ github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIn github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -85,6 +86,12 @@ github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/api/server.go b/pkg/api/server.go index 679ed21..a12b218 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -13,6 +13,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/cors" "github.com/oklog/ulid/v2" + "github.com/xeipuuv/gojsonschema" "go.flipt.io/cup/pkg/api/core" "go.flipt.io/cup/pkg/containers" "go.flipt.io/cup/pkg/controllers" @@ -101,11 +102,11 @@ func (s *Server) addDefinition(def *core.ResourceDefinition, version string) { // Register adds a new controller and definition with a particular filesystem to the server. // This may happen dynamically in the future, so it is guarded with a write lock. -func (s *Server) Register(cntl Controller, def *core.ResourceDefinition) { +func (s *Server) Register(cntl Controller, def *core.ResourceDefinition) error { s.mu.Lock() defer s.mu.Unlock() - for version := range def.Spec.Versions { + for version, schema := range def.Spec.Versions { var ( version = version prefix = fmt.Sprintf("/apis/%s/%s/namespaces/{ns}/%s", def.Spec.Group, version, def.Names.Plural) @@ -115,6 +116,11 @@ func (s *Server) Register(cntl Controller, def *core.ResourceDefinition) { // update sources map s.addDefinition(def, version) + schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(schema)) + if err != nil { + return err + } + // list kind s.mux.Get(prefix, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := s.fs.View(r.Context(), s.rev, func(f fs.FS) error { @@ -173,7 +179,18 @@ func (s *Server) Register(cntl Controller, def *core.ResourceDefinition) { s.mux.Put(named, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var resource core.Resource if err := json.NewDecoder(r.Body).Decode(&resource); err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + res, err := schema.Validate(gojsonschema.NewBytesLoader(resource.Spec)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if !res.Valid() { + http.Error(w, fmt.Sprintf("%v", res.Errors()), http.StatusBadRequest) return } @@ -242,6 +259,8 @@ func (s *Server) Register(cntl Controller, def *core.ResourceDefinition) { } })) } + + return nil } func (s *Server) handleSourceDefinitions(w http.ResponseWriter, r *http.Request) {