first commit
commit
ef4a8021a5
|
@ -0,0 +1 @@
|
||||||
|
dist
|
|
@ -0,0 +1 @@
|
||||||
|
dist
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM golang:1.17.2-alpine3.14 as builder
|
||||||
|
RUN apk update && apk add --no-cache git tzdata
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum /src/
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN VERSION=$(git describe --tags) BUILD=$(git rev-parse --short HEAD) && \
|
||||||
|
GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" \
|
||||||
|
-o /go/bin/configui main.go
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:3.14
|
||||||
|
EXPOSE 5080
|
||||||
|
ENV PATH="/go/bin:${PATH}"
|
||||||
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=builder /go/bin/configui /go/bin/configui
|
||||||
|
CMD ["/go/bin/configui"]
|
|
@ -0,0 +1,13 @@
|
||||||
|
VERSION=$(shell git describe --tags --abbrev=0)
|
||||||
|
BUILD=$(shell git rev-parse --short HEAD)
|
||||||
|
PROJ := $(shell basename "$(PWD)")
|
||||||
|
HUB=hub.kumoly.io
|
||||||
|
HUB_PROJECT=tools
|
||||||
|
|
||||||
|
LDFLAGS=-ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w"
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
go build ${LDFLAGS} -o dist/${PROJ}
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Config UI
|
||||||
|
|
||||||
|
a web app to edit and action on update
|
||||||
|
|
||||||
|
## Api
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
`GET /api/files`
|
||||||
|
|
||||||
|
res:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"test": {
|
||||||
|
"path": "test",
|
||||||
|
"name": "test",
|
||||||
|
"action": "myip local -P",
|
||||||
|
"data": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
`GET /api/file?name=ALIAS`
|
||||||
|
|
||||||
|
res:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "myip local -P",
|
||||||
|
"data": "test",
|
||||||
|
"name": "test",
|
||||||
|
"path": "test"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
`POST /api/file`
|
||||||
|
|
||||||
|
req:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": "test",
|
||||||
|
"name": "test",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apply
|
||||||
|
|
||||||
|
`POST /api/apply?name=ALIAS`
|
|
@ -0,0 +1,77 @@
|
||||||
|
package configui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var UNIX_SHELL = "bash"
|
||||||
|
var WIN_SHELL = "cmd"
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Alias string `json:"name"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
|
||||||
|
// used for parsing post data
|
||||||
|
Data string `json:"data"`
|
||||||
|
|
||||||
|
lock sync.RWMutex `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Read() ([]byte, error) {
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
data, err := os.ReadFile(f.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Write(data []byte) error {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
info, err := os.Stat(f.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(f.Path, data, info.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Do() (string, error) {
|
||||||
|
if f.Action == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
cmd := exec.Command(UNIX_SHELL, "-c", f.Action)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd = exec.Command(WIN_SHELL, "/c", f.Action)
|
||||||
|
}
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(confstr string) ([]File, error) {
|
||||||
|
conf := []File{}
|
||||||
|
err := json.Unmarshal([]byte(confstr), &conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileMap(files []File) map[string]*File {
|
||||||
|
fileMap := map[string]*File{}
|
||||||
|
for i := range files {
|
||||||
|
fileMap[files[i].Alias] = &files[i]
|
||||||
|
}
|
||||||
|
return fileMap
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"kumoly.io/tools/configui/configui"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed templates
|
||||||
|
var tmplFS embed.FS
|
||||||
|
var tmpl *template.Template
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmdPath string
|
||||||
|
cmdAction string
|
||||||
|
cmdAlias string
|
||||||
|
cmdConfigPath string
|
||||||
|
|
||||||
|
cmdBind string
|
||||||
|
)
|
||||||
|
|
||||||
|
var files = map[string]*configui.File{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
flag.StringVar(&cmdConfigPath, "f", "", "path to config file")
|
||||||
|
flag.StringVar(&cmdPath, "p", "", "path to file, precedence over config")
|
||||||
|
flag.StringVar(&cmdAlias, "n", "", "alias of file")
|
||||||
|
flag.StringVar(&cmdAction, "c", "", "cmd to apply")
|
||||||
|
flag.StringVar(&cmdBind, "b", "localhost:8000", "address to bind")
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: configui [options]\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// setup values
|
||||||
|
if cmdPath != "" {
|
||||||
|
if cmdAlias == "" {
|
||||||
|
cmdAlias = cmdPath
|
||||||
|
}
|
||||||
|
files[cmdAlias] = &configui.File{
|
||||||
|
Path: cmdPath,
|
||||||
|
Alias: cmdAlias,
|
||||||
|
Action: cmdAction,
|
||||||
|
}
|
||||||
|
} else if cmdConfigPath == "" {
|
||||||
|
log.Fatalln(errors.New("no config found"))
|
||||||
|
} else {
|
||||||
|
conf, err := os.ReadFile(cmdConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
ftmp, err := configui.ReadConfig(string(conf))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
files = configui.GetFileMap(ftmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup routes
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/api/files", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
ListFiles(rw, r)
|
||||||
|
} else {
|
||||||
|
rw.WriteHeader(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
GetFile(rw, r)
|
||||||
|
} else if r.Method == "POST" {
|
||||||
|
PostFile(rw, r)
|
||||||
|
} else {
|
||||||
|
rw.WriteHeader(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/api/apply", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
Apply(rw, r)
|
||||||
|
} else {
|
||||||
|
rw.WriteHeader(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := tmpl.ExecuteTemplate(buf, "home", nil)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
} else {
|
||||||
|
rw.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: cmdBind,
|
||||||
|
WriteTimeout: time.Second * 3,
|
||||||
|
ReadTimeout: time.Second * 30,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
// start server
|
||||||
|
log.Println("Listening on ", cmdBind)
|
||||||
|
log.Fatal(server.ListenAndServe())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListFiles(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
data, err := json.Marshal(files)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFile(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
name := r.URL.Query().Get("name")
|
||||||
|
file, ok := files[name]
|
||||||
|
if name == "" || !ok {
|
||||||
|
rw.WriteHeader(404)
|
||||||
|
rw.Write([]byte("file not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := file.Read()
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := json.Marshal(map[string]string{
|
||||||
|
"path": file.Path,
|
||||||
|
"name": file.Alias,
|
||||||
|
"action": file.Action,
|
||||||
|
"data": string(data),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Header().Set("Content-Type", "application/json")
|
||||||
|
rw.Write(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostFile(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f := configui.File{}
|
||||||
|
if err := json.Unmarshal(data, &f); err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, ok := files[f.Alias]
|
||||||
|
if !ok {
|
||||||
|
rw.WriteHeader(404)
|
||||||
|
rw.Write([]byte("file not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := file.Write([]byte(f.Data)); err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Apply(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
name := r.URL.Query().Get("name")
|
||||||
|
file, ok := files[name]
|
||||||
|
if name == "" || !ok {
|
||||||
|
rw.WriteHeader(404)
|
||||||
|
rw.Write([]byte("file not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := file.Do()
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Write([]byte(result))
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{{define "base/footer"}}
|
||||||
|
<script src="/public/js/main.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{{define "base/header"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Serviced</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{{define "home"}}
|
||||||
|
{{template "base/header" .}}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Hello World
|
||||||
|
</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
My first website with <strong>Bulma</strong>!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{template "base/footer" .}}
|
||||||
|
{{end}}
|
Loading…
Reference in New Issue