298 lines
6.2 KiB
Go
298 lines
6.2 KiB
Go
package codegen
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/cortezaproject/corteza/server/pkg/handle"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type (
|
|
// definitions are in multiple files and each definition
|
|
// should produce one output
|
|
actionsDef struct {
|
|
Component string
|
|
Source string
|
|
outputDir string
|
|
|
|
// List of imports
|
|
// Used only by generated file and not pre-generated-user-file
|
|
Import []string `yaml:"import"`
|
|
|
|
Service string `yaml:"service"`
|
|
Resource string `yaml:"resource"`
|
|
|
|
// Default severity for actions
|
|
DefaultActionSeverity string `yaml:"defaultActionSeverity"`
|
|
|
|
// Default severity for errors
|
|
DefaultErrorSeverity string `yaml:"defaultErrorSeverity"`
|
|
|
|
// If at least one of the errors has HTTP status defined,
|
|
// add support for http errors
|
|
SupportHttpErrors bool
|
|
|
|
Props []*propsDef
|
|
Actions []*actionDef
|
|
Errors []*errorDef
|
|
}
|
|
|
|
// List of event/log properties that can/will be captured
|
|
// and injected into log or message string
|
|
propsDef struct {
|
|
Name string
|
|
Type string
|
|
Fields []string
|
|
Builtin bool
|
|
}
|
|
|
|
actionDef struct {
|
|
// Action name
|
|
Action string `yaml:"action"`
|
|
|
|
// String to log when action is successful
|
|
Log string `yaml:"log"`
|
|
|
|
// String to log when error was yield
|
|
//ErrorLog string `yaml:"errorLog"`
|
|
|
|
// Action severity
|
|
Severity string `yaml:"severity"`
|
|
}
|
|
|
|
// Event definition
|
|
errorDef struct {
|
|
// Error key
|
|
// message can contain {variables} from meta data
|
|
Error string `yaml:"error"`
|
|
|
|
// Error key
|
|
// message can contain {variables} from meta data
|
|
Message string `yaml:"message"`
|
|
|
|
// Formatted and readable audit log message
|
|
// message can contain {variables} from meta data
|
|
Log string `yaml:"log"`
|
|
|
|
// Longer message or error description that can help resolving the error
|
|
Details string `yaml:"details"`
|
|
|
|
// Relative link to content in the documentation
|
|
Documentation string `yaml:"documentation"`
|
|
|
|
// Reference to "safe" error
|
|
// safe error should hide any information that might cause
|
|
// personal data leakage or expose system internals
|
|
MaskedWith string `yaml:"maskedWith"`
|
|
|
|
// Error severity
|
|
Severity string `yaml:"severity"`
|
|
|
|
// HTTP Status code for this error
|
|
HttpStatus string `yaml:"httpStatus"`
|
|
}
|
|
)
|
|
|
|
// Processes multiple action definitions
|
|
func procActions(mm ...string) (dd []*actionsDef, err error) {
|
|
var (
|
|
f io.ReadCloser
|
|
d *actionsDef
|
|
)
|
|
|
|
dd = make([]*actionsDef, 0)
|
|
for _, m := range mm {
|
|
err = func() error {
|
|
|
|
if f, err = os.Open(m); err != nil {
|
|
return err
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
d = &actionsDef{Component: strings.SplitN(m, string(filepath.Separator), 2)[0]}
|
|
|
|
if err := yaml.NewDecoder(f).Decode(d); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = actionNormalize(d); err != nil {
|
|
return err
|
|
}
|
|
|
|
d.Source = m
|
|
d.outputDir = path.Dir(m)
|
|
|
|
dd = append(dd, d)
|
|
return nil
|
|
}()
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not process %s: %w", m, err)
|
|
}
|
|
}
|
|
|
|
return dd, nil
|
|
}
|
|
|
|
func actionNormalize(d *actionsDef) error {
|
|
// Prepend generic error
|
|
d.Errors = append([]*errorDef{{
|
|
Error: "generic",
|
|
Message: "failed to complete request due to internal error",
|
|
Log: "{err}",
|
|
Severity: "error",
|
|
}}, d.Errors...)
|
|
|
|
// index known meta fields and sanitize types (no type => string type)
|
|
knownProps := map[string]bool{
|
|
"err": true,
|
|
}
|
|
|
|
for _, m := range d.Props {
|
|
if m.Type == "" {
|
|
m.Type = "string"
|
|
}
|
|
|
|
// very optimistic check if referenced type is builtin or not
|
|
m.Builtin = !strings.Contains(m.Type, ".")
|
|
|
|
knownProps[m.Name] = true
|
|
|
|
for _, f := range m.Fields {
|
|
knownProps[fmt.Sprintf("%s.%s", m.Name, f)] = true
|
|
|
|
}
|
|
}
|
|
|
|
for _, a := range d.Actions {
|
|
if a.Severity == "" {
|
|
a.Severity = d.DefaultActionSeverity
|
|
}
|
|
}
|
|
|
|
for _, e := range d.Errors {
|
|
if e.Severity == "" {
|
|
e.Severity = d.DefaultErrorSeverity
|
|
}
|
|
|
|
if e.HttpStatus != "" {
|
|
d.SupportHttpErrors = true
|
|
}
|
|
}
|
|
|
|
checkHandle := func(s string) error {
|
|
if !handle.IsValid(s) {
|
|
return fmt.Errorf("handle empty")
|
|
|
|
}
|
|
|
|
if !handle.IsValid(s) {
|
|
return fmt.Errorf("invalid handle format: %q", s)
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
placeholderMatcher := regexp.MustCompile(`\{\{(.+?)\}\}`)
|
|
checkPlaceholders := func(def string, kind, s string) error {
|
|
for _, match := range placeholderMatcher.FindAllStringSubmatch(s, 1) {
|
|
placeholder := match[1]
|
|
if !knownProps[placeholder] {
|
|
return fmt.Errorf("unknown placeholder %q used in %s for %s", placeholder, def, kind)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, a := range d.Actions {
|
|
checkHandle(a.Action)
|
|
if a.Log == "" {
|
|
// If no log is defined, use action handle
|
|
a.Log = a.Action
|
|
}
|
|
|
|
if err := checkPlaceholders(a.Action, "log", a.Log); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, e := range d.Errors {
|
|
if err := checkHandle(e.Error); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkPlaceholders(e.Error, "message", e.Message); err != nil {
|
|
return err
|
|
}
|
|
if err := checkPlaceholders(e.Error, "log", e.Log); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a actionsDef) Package() string {
|
|
return path.Base(path.Dir(a.Source))
|
|
}
|
|
|
|
func (a actionDef) SeverityConstName() string {
|
|
return severityConstName(a.Severity)
|
|
}
|
|
|
|
func (e errorDef) SeverityConstName() string {
|
|
return severityConstName(e.Severity)
|
|
}
|
|
|
|
func severityConstName(s string) string {
|
|
switch strings.ToLower(s) {
|
|
case "emergency":
|
|
return "actionlog.Emergency"
|
|
case "alert":
|
|
return "actionlog.Alert"
|
|
case "crit", "critical":
|
|
return "actionlog.Critical"
|
|
case "warn", "warning":
|
|
return "actionlog.Warning"
|
|
case "notice":
|
|
return "actionlog.Notice"
|
|
case "info", "informational":
|
|
return "actionlog.Info"
|
|
case "debug":
|
|
return "actionlog.Debug"
|
|
default:
|
|
return "actionlog.Err"
|
|
}
|
|
}
|
|
|
|
func genActions(tpl *template.Template, dd ...*actionsDef) (err error) {
|
|
var (
|
|
// Will only be generated if file does not exist previously
|
|
tplActionsGen = tpl.Lookup("actions.gen.go.tpl")
|
|
|
|
dst string
|
|
)
|
|
|
|
for _, d := range dd {
|
|
// Generic code, actions for every resource goes to a separated file
|
|
dst = path.Join(d.outputDir, path.Base(d.Source)[:strings.LastIndex(path.Base(d.Source), ".")]+".gen.go")
|
|
err = goTemplate(dst, tplActionsGen, d)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|