configui/configui.go

323 lines
6.7 KiB
Go
Raw Normal View History

package configui
import (
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
2021-11-03 19:35:51 +00:00
"os"
2021-11-12 09:25:44 +00:00
"path/filepath"
2021-11-12 15:09:32 +00:00
"runtime"
2021-11-09 03:27:58 +00:00
"strings"
2021-11-05 03:10:13 +00:00
"sync"
2021-11-03 10:08:37 +00:00
"time"
2021-11-03 19:35:51 +00:00
"kumoly.io/lib/klog"
2021-11-04 05:55:45 +00:00
"kumoly.io/lib/ksrv/engine"
"kumoly.io/tools/configui/public"
)
2021-11-12 15:09:32 +00:00
const UNIX_SHELL = "/usr/bin/sh"
const WIN_SHELL = "C:\\Windows\\System32\\cmd"
const DARWIN_SHELL = "/bin/bash"
2021-11-12 07:08:34 +00:00
const version = "v0.1.12"
2021-10-23 16:49:44 +00:00
//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",
2021-10-24 06:32:55 +00:00
"md": "markdown",
}
2021-11-09 03:27:58 +00:00
type Action struct {
2021-11-12 15:09:32 +00:00
Name string `json:"name"`
Cmd string `json:"cmd"`
Dir string `json:"dir"`
2021-11-12 09:25:44 +00:00
run chan struct{} `json:"-"`
2021-11-12 15:09:32 +00:00
pid int
2021-11-09 03:27:58 +00:00
}
type Integration struct {
Name string `json:"name"`
Description string `json:"description"`
Cmd func() (string, error) `json:"-"`
2021-11-11 01:58:36 +00:00
run chan struct{} `json:"-"`
2021-11-09 03:27:58 +00:00
}
type ConfigUI struct {
AppName string `json:"app_name"`
2021-11-03 19:35:51 +00:00
Prod bool `json:"production"`
2021-10-24 07:06:02 +00:00
BaseUrl string `json:"base_url"`
ConfigPath string `json:"config_path"`
2021-11-12 15:09:32 +00:00
SHELL string `json:"shell"`
NoReconfig bool `json:"no_reconfig"`
AllowIP string `json:"allow_ip"`
2021-11-03 10:08:37 +00:00
CmdTimeout string `json:"timeout"`
cmdTimeout time.Duration
2021-11-09 03:27:58 +00:00
Files []*File `json:"files"`
fileIndex map[string]int
Actions []Action `json:"actions"`
2021-11-09 03:41:41 +00:00
Integrations []*Integration `json:"integrations,omitempty"`
ResultBellow bool `json:"result_bellow"`
HideConfig bool `json:"hide_config"`
// Should be in main app
2021-11-05 16:09:51 +00:00
LogPath string `json:"log_path"`
LogLevel klog.Llevel `json:"log_level"`
2021-11-12 09:25:44 +00:00
// Running commands
2021-11-13 04:59:08 +00:00
Onitachi map[string]*Oni `json:"onitachi"`
2021-11-12 09:25:44 +00:00
2021-11-05 03:10:13 +00:00
TmplFS embed.FS `json:"-"`
tmpl *engine.Engine
2021-11-09 03:27:58 +00:00
PublicFS embed.FS `json:"-"`
log *klog.Logger
ksrv_log *klog.Logger
f *os.File
configLock sync.Mutex
}
func New() *ConfigUI {
2021-11-04 05:55:45 +00:00
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
},
2021-11-09 03:27:58 +00:00
"normal": func(name string) string {
return strings.ReplaceAll(name, " ", "-")
},
2021-11-12 15:09:32 +00:00
"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"))
2021-11-12 15:09:32 +00:00
sh := ""
switch runtime.GOOS {
case "darwin":
sh = DARWIN_SHELL
case "windows":
sh = WIN_SHELL
default:
sh = UNIX_SHELL
}
return &ConfigUI{
2021-11-03 10:08:37 +00:00
fileIndex: map[string]int{},
2021-11-12 15:09:32 +00:00
SHELL: sh,
2021-11-03 19:35:51 +00:00
Prod: true,
2021-11-03 10:08:37 +00:00
AppName: "ConfigUI",
BaseUrl: "/",
2021-11-09 03:27:58 +00:00
Actions: []Action{{Name: "User", Cmd: "whoami"}},
2021-11-03 10:08:37 +00:00
PublicFS: public.FS,
TmplFS: tmplFS,
tmpl: tmpl,
CmdTimeout: "10s",
cmdTimeout: time.Second * 10,
2021-11-03 19:35:51 +00:00
LogLevel: klog.Lerror | klog.Linfo,
log: klog.Sub("ConfigUI"),
2021-11-13 04:59:08 +00:00
Onitachi: make(map[string]*Oni),
}
}
func (cui *ConfigUI) File(file_name string) (*File, error) {
if file_name == cui.AppName {
return &File{
2021-11-03 19:35:51 +00:00
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 {
2021-11-13 04:59:08 +00:00
cui.configLock.Lock()
defer cui.configLock.Unlock()
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
}
2021-11-03 19:35:51 +00:00
f.owner = cui
tmpIndex[f.Name] = i
// deprecated fix
if f.Cmd == "" && f.Action != "" {
f.Cmd = f.Action
f.Action = ""
}
}
2021-11-13 04:59:08 +00:00
// del oni dry run
for k, v := range cui.Onitachi {
_, ok := tmpConf.Onitachi[k]
if !ok && v.State == STARTED {
return ErrorNoDeleteRunning.New(k)
2021-11-12 15:09:32 +00:00
}
}
// copy
cui.fileIndex = tmpIndex
cui.Files = tmpConf.Files
cui.AllowIP = tmpConf.AllowIP
2021-11-03 19:35:51 +00:00
cui.Prod = tmpConf.Prod
klog.PROD = cui.Prod
cui.ConfigPath = tmpConf.ConfigPath
cui.HideConfig = tmpConf.HideConfig
cui.NoReconfig = tmpConf.NoReconfig
2021-11-05 03:10:13 +00:00
cui.ResultBellow = tmpConf.ResultBellow
2021-11-12 15:09:32 +00:00
cui.SHELL = tmpConf.SHELL
2021-11-03 19:35:51 +00:00
2021-11-13 04:59:08 +00:00
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()
// construct oni - update existing, del
for k, v := range cui.Onitachi {
n, ok := tmpConf.Onitachi[k]
if !ok {
delete(cui.Onitachi, k)
} else {
v.Name = k
v.Cmd = n.Cmd
v.Args = n.Args
v.Envs = n.Envs
v.Dir = n.Dir
v.Policy = n.Policy
}
}
// construct oni - append new
for k, v := range tmpConf.Onitachi {
_, ok := cui.Onitachi[k]
if !ok {
v.Name = k
v.Init(cui)
cui.Onitachi[k] = v
}
}
2021-11-09 03:27:58 +00:00
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
2021-11-03 19:35:51 +00:00
if cui.AppName == "" {
cui.AppName = "ConfigUI"
}
2021-10-24 07:06:02 +00:00
cui.BaseUrl = tmpConf.BaseUrl
2021-10-24 07:15:21 +00:00
if cui.BaseUrl == "" {
cui.BaseUrl = "/"
}
2021-11-03 19:35:51 +00:00
2021-11-03 10:08:37 +00:00
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
}
// fmt.Printf("%+v", cui)
return nil
}
2021-11-03 19:35:51 +00:00
func (cui *ConfigUI) setLog() {
var err error
if cui.f != nil {
cui.f.Close()
}
if cui.LogPath != "" {
2021-11-12 09:25:44 +00:00
mkdir(filepath.Dir(cui.LogPath))
2021-11-03 19:35:51 +00:00
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
}
2021-11-03 19:35:51 +00:00
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
}