diff --git a/api/compose/spec.json b/api/compose/spec.json index b1b75a332..196f166f1 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -1658,6 +1658,31 @@ ] } }, + { + "name": "bundle", + "method": "GET", + "title": "Serves client scripts bundle", + "path": "/{bundle}-{type}.{ext}", + "parameters": { + "path": [ + { + "name": "bundle", + "type": "string", + "title": "Name of the bundle" + }, + { + "name": "type", + "type": "string", + "title": "Bundle type" + }, + { + "name": "ext", + "type": "string", + "title": "Bundle extension" + } + ] + } + }, { "name": "triggerScript", "method": "POST", diff --git a/api/compose/spec/automation.json b/api/compose/spec/automation.json index e924f40cb..545766e37 100644 --- a/api/compose/spec/automation.json +++ b/api/compose/spec/automation.json @@ -47,6 +47,31 @@ ] } }, + { + "Name": "bundle", + "Method": "GET", + "Title": "Serves client scripts bundle", + "Path": "/{bundle}-{type}.{ext}", + "Parameters": { + "path": [ + { + "name": "bundle", + "title": "Name of the bundle", + "type": "string" + }, + { + "name": "type", + "title": "Bundle type", + "type": "string" + }, + { + "name": "ext", + "title": "Bundle extension", + "type": "string" + } + ] + } + }, { "Name": "triggerScript", "Method": "POST", diff --git a/api/system/spec.json b/api/system/spec.json index 3b6b78320..332231234 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -1727,6 +1727,31 @@ ] } }, + { + "name": "bundle", + "method": "GET", + "title": "Serves client scripts bundle", + "path": "/{bundle}-{type}.{ext}", + "parameters": { + "path": [ + { + "name": "bundle", + "type": "string", + "title": "Name of the bundle" + }, + { + "name": "type", + "type": "string", + "title": "Bundle type" + }, + { + "name": "ext", + "type": "string", + "title": "Bundle extension" + } + ] + } + }, { "name": "triggerScript", "method": "POST", diff --git a/api/system/spec/automation.json b/api/system/spec/automation.json index 951d842c5..5509e5929 100644 --- a/api/system/spec/automation.json +++ b/api/system/spec/automation.json @@ -47,6 +47,31 @@ ] } }, + { + "Name": "bundle", + "Method": "GET", + "Title": "Serves client scripts bundle", + "Path": "/{bundle}-{type}.{ext}", + "Parameters": { + "path": [ + { + "name": "bundle", + "title": "Name of the bundle", + "type": "string" + }, + { + "name": "type", + "title": "Bundle type", + "type": "string" + }, + { + "name": "ext", + "title": "Bundle extension", + "type": "string" + } + ] + } + }, { "Name": "triggerScript", "Method": "POST", diff --git a/compose/rest/automation.go b/compose/rest/automation.go index 02c7e84bd..4040152d7 100644 --- a/compose/rest/automation.go +++ b/compose/rest/automation.go @@ -37,6 +37,16 @@ func (ctrl *Automation) List(ctx context.Context, r *request.AutomationList) (in ) } +func (ctrl *Automation) Bundle(ctx context.Context, r *request.AutomationBundle) (interface{}, error) { + return corredor.GenericBundleHandler( + ctx, + corredor.Service(), + r.Bundle, + r.Type, + r.Ext, + ) +} + func (ctrl *Automation) TriggerScript(ctx context.Context, r *request.AutomationTriggerScript) (interface{}, error) { return resputil.OK(), corredor.Service().ExecOnManual(ctx, r.Script, event.ComposeOnManual()) } diff --git a/compose/rest/handlers/automation.go b/compose/rest/handlers/automation.go index dffe5f45c..1453deaee 100644 --- a/compose/rest/handlers/automation.go +++ b/compose/rest/handlers/automation.go @@ -30,12 +30,14 @@ import ( // Internal API interface type AutomationAPI interface { List(context.Context, *request.AutomationList) (interface{}, error) + Bundle(context.Context, *request.AutomationBundle) (interface{}, error) TriggerScript(context.Context, *request.AutomationTriggerScript) (interface{}, error) } // HTTP API interface type Automation struct { List func(http.ResponseWriter, *http.Request) + Bundle func(http.ResponseWriter, *http.Request) TriggerScript func(http.ResponseWriter, *http.Request) } @@ -61,6 +63,26 @@ func NewAutomation(h AutomationAPI) *Automation { resputil.JSON(w, value) } }, + Bundle: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationBundle() + if err := params.Fill(r); err != nil { + logger.LogParamError("Automation.Bundle", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Bundle(r.Context(), params) + if err != nil { + logger.LogControllerError("Automation.Bundle", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Automation.Bundle", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, TriggerScript: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewAutomationTriggerScript() @@ -88,6 +110,7 @@ func (h Automation) MountRoutes(r chi.Router, middlewares ...func(http.Handler) r.Group(func(r chi.Router) { r.Use(middlewares...) r.Get("/automation/", h.List) + r.Get("/automation/{bundle}-{type}.{ext}", h.Bundle) r.Post("/automation/trigger", h.TriggerScript) }) } diff --git a/compose/rest/request/automation.go b/compose/rest/request/automation.go index 4c3a0262a..787703571 100644 --- a/compose/rest/request/automation.go +++ b/compose/rest/request/automation.go @@ -117,6 +117,63 @@ func (r *AutomationList) Fill(req *http.Request) (err error) { var _ RequestFiller = NewAutomationList() +// Automation bundle request parameters +type AutomationBundle struct { + Bundle string + Type string + Ext string +} + +func NewAutomationBundle() *AutomationBundle { + return &AutomationBundle{} +} + +func (r AutomationBundle) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["bundle"] = r.Bundle + out["type"] = r.Type + out["ext"] = r.Ext + + return out +} + +func (r *AutomationBundle) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.Bundle = chi.URLParam(req, "bundle") + r.Type = chi.URLParam(req, "type") + r.Ext = chi.URLParam(req, "ext") + + return err +} + +var _ RequestFiller = NewAutomationBundle() + // Automation triggerScript request parameters type AutomationTriggerScript struct { Script string diff --git a/docs/compose/README.md b/docs/compose/README.md index 1d1ca22bb..f07df9e5c 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -116,6 +116,7 @@ | Method | Endpoint | Purpose | | ------ | -------- | ------- | | `GET` | `/automation/` | List all available automation scripts for compose resources | +| `GET` | `/automation/{bundle}-{type}.{ext}` | Serves client scripts bundle | | `POST` | `/automation/trigger` | Triggers execution of a specific script on a system service level | ## List all available automation scripts for compose resources @@ -139,6 +140,24 @@ Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.cru | excludeClientScripts | bool | GET | Do not include client scripts | N/A | NO | | excludeServerScripts | bool | GET | Do not include server scripts | N/A | NO | +## Serves client scripts bundle + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/{bundle}-{type}.{ext}` | HTTP/S | GET | +Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.crust/corteza-server/codegen/templates/README.tpl on line 32 + | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| bundle | string | PATH | Name of the bundle | N/A | NO | +| type | string | PATH | Bundle type | N/A | NO | +| ext | string | PATH | Bundle extension | N/A | NO | + ## Triggers execution of a specific script on a system service level #### Method diff --git a/docs/system/README.md b/docs/system/README.md index 5fcc83bc8..4d59b16f5 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -320,6 +320,7 @@ | Method | Endpoint | Purpose | | ------ | -------- | ------- | | `GET` | `/automation/` | List all available automation scripts for system resources | +| `GET` | `/automation/{bundle}-{type}.{ext}` | Serves client scripts bundle | | `POST` | `/automation/trigger` | Triggers execution of a specific script on a system service level | ## List all available automation scripts for system resources @@ -343,6 +344,24 @@ Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.cru | excludeClientScripts | bool | GET | Do not include client scripts | N/A | NO | | excludeServerScripts | bool | GET | Do not include server scripts | N/A | NO | +## Serves client scripts bundle + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/{bundle}-{type}.{ext}` | HTTP/S | GET | +Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.crust/corteza-server/codegen/templates/README.tpl on line 32 + | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| bundle | string | PATH | Name of the bundle | N/A | NO | +| type | string | PATH | Bundle type | N/A | NO | +| ext | string | PATH | Bundle extension | N/A | NO | + ## Triggers execution of a specific script on a system service level #### Method diff --git a/pkg/corredor/service.go b/pkg/corredor/service.go index e0f1a9c6f..029175c13 100644 --- a/pkg/corredor/service.go +++ b/pkg/corredor/service.go @@ -673,3 +673,29 @@ func (svc *service) registerClientScripts(ss ...*ClientScript) { } } } + +func (svc *service) GetBundle(ctx context.Context, name, bType string) *Bundle { + var ( + err error + rsp *BundleResponse + ) + + svc.log.Debug("reloading server scripts") + + ctx, cancel := context.WithTimeout(ctx, svc.opt.ListTimeout) + defer cancel() + + rsp, err = svc.csClient.Bundle(ctx, &BundleRequest{Name: name}, grpc.WaitForReady(true)) + if err != nil { + svc.log.Error("could not load corredor server scripts", zap.Error(err)) + return nil + } + + for _, b := range rsp.Bundles { + if b.Type == bType { + return b + } + } + + return nil +} diff --git a/pkg/corredor/util.go b/pkg/corredor/util.go index a519aa130..2e6779e02 100644 --- a/pkg/corredor/util.go +++ b/pkg/corredor/util.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/pkg/errors" + "net/http" ) type ( @@ -99,3 +100,17 @@ func GenericListHandler(ctx context.Context, svc *service, f Filter, resourcePre p.Set, p.Filter, err = svc.Find(ctx, f) return p, err } + +func GenericBundleHandler(ctx context.Context, svc *service, bundleName, bundleType, ext string) (interface{}, error) { + return func(w http.ResponseWriter, req *http.Request) { + // Serve bundle directly for now + bundle := svc.GetBundle(ctx, bundleName, bundleType) + if bundle == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(bundle.Code)) + }, nil +} diff --git a/system/rest/automation.go b/system/rest/automation.go index 961039a44..52b114bdb 100644 --- a/system/rest/automation.go +++ b/system/rest/automation.go @@ -37,6 +37,16 @@ func (ctrl *Automation) List(ctx context.Context, r *request.AutomationList) (in ) } +func (ctrl *Automation) Bundle(ctx context.Context, r *request.AutomationBundle) (interface{}, error) { + return corredor.GenericBundleHandler( + ctx, + corredor.Service(), + r.Bundle, + r.Type, + r.Ext, + ) +} + func (ctrl *Automation) TriggerScript(ctx context.Context, r *request.AutomationTriggerScript) (interface{}, error) { return resputil.OK(), corredor.Service().ExecOnManual(ctx, r.Script, event.SystemOnManual()) } diff --git a/system/rest/handlers/automation.go b/system/rest/handlers/automation.go index b3473a334..29e063a5e 100644 --- a/system/rest/handlers/automation.go +++ b/system/rest/handlers/automation.go @@ -30,12 +30,14 @@ import ( // Internal API interface type AutomationAPI interface { List(context.Context, *request.AutomationList) (interface{}, error) + Bundle(context.Context, *request.AutomationBundle) (interface{}, error) TriggerScript(context.Context, *request.AutomationTriggerScript) (interface{}, error) } // HTTP API interface type Automation struct { List func(http.ResponseWriter, *http.Request) + Bundle func(http.ResponseWriter, *http.Request) TriggerScript func(http.ResponseWriter, *http.Request) } @@ -61,6 +63,26 @@ func NewAutomation(h AutomationAPI) *Automation { resputil.JSON(w, value) } }, + Bundle: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationBundle() + if err := params.Fill(r); err != nil { + logger.LogParamError("Automation.Bundle", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Bundle(r.Context(), params) + if err != nil { + logger.LogControllerError("Automation.Bundle", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Automation.Bundle", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, TriggerScript: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewAutomationTriggerScript() @@ -88,6 +110,7 @@ func (h Automation) MountRoutes(r chi.Router, middlewares ...func(http.Handler) r.Group(func(r chi.Router) { r.Use(middlewares...) r.Get("/automation/", h.List) + r.Get("/automation/{bundle}-{type}.{ext}", h.Bundle) r.Post("/automation/trigger", h.TriggerScript) }) } diff --git a/system/rest/request/automation.go b/system/rest/request/automation.go index 4c3a0262a..787703571 100644 --- a/system/rest/request/automation.go +++ b/system/rest/request/automation.go @@ -117,6 +117,63 @@ func (r *AutomationList) Fill(req *http.Request) (err error) { var _ RequestFiller = NewAutomationList() +// Automation bundle request parameters +type AutomationBundle struct { + Bundle string + Type string + Ext string +} + +func NewAutomationBundle() *AutomationBundle { + return &AutomationBundle{} +} + +func (r AutomationBundle) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["bundle"] = r.Bundle + out["type"] = r.Type + out["ext"] = r.Ext + + return out +} + +func (r *AutomationBundle) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.Bundle = chi.URLParam(req, "bundle") + r.Type = chi.URLParam(req, "type") + r.Ext = chi.URLParam(req, "ext") + + return err +} + +var _ RequestFiller = NewAutomationBundle() + // Automation triggerScript request parameters type AutomationTriggerScript struct { Script string