configui/configui.go

323 lines
6.7 KiB
Go

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 map[string]*Oni `json:"onitachi"`
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"),
Onitachi: make(map[string]*Oni),
}
}
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 {
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
}
f.owner = cui
tmpIndex[f.Name] = i
// deprecated fix
if f.Cmd == "" && f.Action != "" {
f.Cmd = f.Action
f.Action = ""
}
}
// del oni dry run
for k, v := range cui.Onitachi {
_, ok := tmpConf.Onitachi[k]
if !ok && v.State == STARTED {
return ErrorNoDeleteRunning.New(k)
}
}
// copy
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.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
}
}
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
}
// 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
}