package configui import ( "embed" "encoding/json" "errors" "fmt" "html/template" "os" "path/filepath" "runtime" "strings" "sync" "time" "kumoly.io/lib/klog" "kumoly.io/lib/ksrv/engine" "kumoly.io/tools/configui/public" ) const UNIX_SHELL = "/usr/bin/sh" const WIN_SHELL = "C:\\Windows\\System32\\cmd" const DARWIN_SHELL = "/bin/bash" const version = "v0.1.12" //go:embed templates var tmplFS embed.FS var Ext2Mode map[string]string = map[string]string{ "go": "golang", "log": "sh", "txt": "text", "yml": "yaml", "conf": "ini", "md": "markdown", } type Action struct { Name string `json:"name"` Cmd string `json:"cmd"` Dir string `json:"dir"` run chan struct{} `json:"-"` pid int } type Integration struct { Name string `json:"name"` Description string `json:"description"` Cmd func() (string, error) `json:"-"` run chan struct{} `json:"-"` } type ConfigUI struct { AppName string `json:"app_name"` Prod bool `json:"production"` BaseUrl string `json:"base_url"` ConfigPath string `json:"config_path"` SHELL string `json:"shell"` NoReconfig bool `json:"no_reconfig"` AllowIP string `json:"allow_ip"` CmdTimeout string `json:"timeout"` cmdTimeout time.Duration Files []*File `json:"files"` fileIndex map[string]int Actions []Action `json:"actions"` Integrations []*Integration `json:"integrations,omitempty"` ResultBellow bool `json:"result_bellow"` HideConfig bool `json:"hide_config"` // Should be in main app LogPath string `json:"log_path"` LogLevel klog.Llevel `json:"log_level"` // Running commands Onitachi []*Oni `json:"onitachi"` oniIndex map[string]int `json:"-"` TmplFS embed.FS `json:"-"` tmpl *engine.Engine PublicFS embed.FS `json:"-"` log *klog.Logger ksrv_log *klog.Logger f *os.File configLock sync.Mutex } func New() *ConfigUI { tmpl := engine.Must(engine.New("").Funcs(template.FuncMap{ "step": func(from, to, step uint) []uint { items := []uint{} for i := from; i <= to; i += step { items = append(items, i) } return items }, "normal": func(name string) string { return strings.ReplaceAll(name, " ", "-") }, "state_class": func(state State) string { switch state { case STARTED: return "is-success" case ERROR: return "is-danger" case ENDED: return "has-background-grey-light" default: return "" } }, "state_icon": func(state State) string { switch state { case STARTED: return "play_arrow" case ERROR: return "error" case ENDED: return "stop" default: return "pending" } }, }).ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl")) sh := "" switch runtime.GOOS { case "darwin": sh = DARWIN_SHELL case "windows": sh = WIN_SHELL default: sh = UNIX_SHELL } return &ConfigUI{ fileIndex: map[string]int{}, SHELL: sh, Prod: true, AppName: "ConfigUI", BaseUrl: "/", Actions: []Action{{Name: "User", Cmd: "whoami"}}, PublicFS: public.FS, TmplFS: tmplFS, tmpl: tmpl, CmdTimeout: "10s", cmdTimeout: time.Second * 10, LogLevel: klog.Lerror | klog.Linfo, log: klog.Sub("ConfigUI"), oniIndex: make(map[string]int), } } func (cui *ConfigUI) File(file_name string) (*File, error) { if file_name == cui.AppName { return &File{ Path: cui.ConfigPath, Name: cui.AppName, Lang: "json", owner: cui, }, nil } index, ok := cui.fileIndex[file_name] if !ok { return nil, errors.New("no file found") } return cui.Files[index], nil } func (cui *ConfigUI) LoadConfig(confstr string) error { tmpConf := &ConfigUI{} err := json.Unmarshal([]byte(confstr), tmpConf) if err != nil { return nil } // construct fileIndex tmpIndex := map[string]int{} for i, f := range tmpConf.Files { if f.Name == "" { f.Name = f.Path } f.owner = cui tmpIndex[f.Name] = i // deprecated fix if f.Cmd == "" && f.Action != "" { f.Cmd = f.Action f.Action = "" } } // construct oni for _, o := range tmpConf.Onitachi { if o.Cmd == "" { continue } if o.Name == "" { o.Name = o.Cmd } // update if exist j, ok := cui.oniIndex[o.Name] if ok { cui.Onitachi[j].Cmd = o.Cmd cui.Onitachi[j].Args = o.Args cui.Onitachi[j].Envs = o.Envs cui.Onitachi[j].Dir = o.Dir cui.Onitachi[j].Policy = o.Policy } else { cui.oniIndex[o.Name] = len(cui.Onitachi) cui.Onitachi = append(cui.Onitachi, o) } } // copy cui.configLock.Lock() defer cui.configLock.Unlock() cui.fileIndex = tmpIndex cui.Files = tmpConf.Files cui.AllowIP = tmpConf.AllowIP cui.Prod = tmpConf.Prod klog.PROD = cui.Prod cui.ConfigPath = tmpConf.ConfigPath cui.HideConfig = tmpConf.HideConfig cui.NoReconfig = tmpConf.NoReconfig cui.ResultBellow = tmpConf.ResultBellow cui.SHELL = tmpConf.SHELL cui.Actions = tmpConf.Actions for i := range cui.Actions { if cui.Actions[i].Name == "" { cui.Actions[i].Name = cui.Actions[i].Cmd } } cui.AppName = tmpConf.AppName if cui.AppName == "" { cui.AppName = "ConfigUI" } cui.BaseUrl = tmpConf.BaseUrl if cui.BaseUrl == "" { cui.BaseUrl = "/" } ct, err := time.ParseDuration(tmpConf.CmdTimeout) if err != nil || cui.CmdTimeout == "" { cui.CmdTimeout = "10s" cui.cmdTimeout = time.Second * 10 } else { cui.CmdTimeout = tmpConf.CmdTimeout cui.cmdTimeout = ct } cui.log = klog.Sub(cui.AppName) cui.LogLevel = tmpConf.LogLevel if cui.LogLevel == 0 { cui.LogLevel = klog.Lerror | klog.Linfo } klog.LEVEL = cui.LogLevel cui.LogPath = tmpConf.LogPath cui.setLog() // fmt.Printf("%+v", cui) return nil } func (cui *ConfigUI) setLog() { var err error if cui.f != nil { cui.f.Close() } if cui.LogPath != "" { mkdir(filepath.Dir(cui.LogPath)) cui.f, err = os.OpenFile(cui.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { cui.log.Error(err) } cui.log.SetErrOutput(cui.f) cui.log.SetOutput(cui.f) if cui.ksrv_log != nil { cui.ksrv_log.SetErrOutput(cui.f) cui.ksrv_log.SetOutput(cui.f) } } else { cui.log.SetErrOutput(os.Stderr) cui.log.SetOutput(os.Stderr) if cui.ksrv_log != nil { cui.ksrv_log.SetErrOutput(os.Stderr) cui.ksrv_log.SetOutput(os.Stderr) } } } func (cui *ConfigUI) Config() ([]byte, error) { return json.MarshalIndent(cui, "", " ") } func (cui *ConfigUI) AppendFile(file *File) error { if file.Name == "" { file.Name = file.Path } file.owner = cui i, ok := cui.fileIndex[file.Name] if ok { return fmt.Errorf("%v already exists at %d", file.Name, i) } cui.fileIndex[file.Name] = len(cui.Files) cui.Files = append(cui.Files, file) return nil }