mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-17 07:17:41 +00:00
seedling: Webui installer (#587)
* 🌱 Add webui Signed-off-by: mudler <mudler@c3os.io> * 🌱 Re-read config files after loading bundles Signed-off-by: mudler <mudler@c3os.io> * [check-spelling] Update metadata Update for https://github.com/kairos-io/kairos/actions/runs/3806058276/attempts/1 Accepted in https://github.com/kairos-io/kairos/pull/587#issuecomment-1367859480 Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com> Signed-off-by: mudler <mudler@c3os.io> * 🎨 Beautify index page Signed-off-by: mudler <mudler@c3os.io> * Do not rerun if we were successful or we are already running Signed-off-by: mudler <mudler@c3os.io> * Add syntax highlight Signed-off-by: mudler <mudler@c3os.io> * Add error message Signed-off-by: mudler <mudler@c3os.io> * Add YAML validation and highlight Signed-off-by: mudler <mudler@c3os.io> * Fixup terminal output Signed-off-by: mudler <mudler@c3os.io> * Fix newlines Signed-off-by: mudler <mudler@c3os.io> * fixups Signed-off-by: mudler <mudler@c3os.io> * 🎨 Fixup lint issues Signed-off-by: mudler <mudler@c3os.io> * Mark dependencies Signed-off-by: mudler <mudler@c3os.io> * Let configure the listening address Signed-off-by: mudler <mudler@c3os.io> Signed-off-by: mudler <mudler@c3os.io> Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
This commit is contained in:
committed by
Itxaka
parent
34c8ad827f
commit
d59892c5c5
@@ -75,6 +75,12 @@ func Run(opts ...Option) error {
|
|||||||
if c.FailOnBundleErrors && err != nil {
|
if c.FailOnBundleErrors && err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-read config files
|
||||||
|
c, err = config.Scan(config.Directories(o.Dir...))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = bus.Manager.Publish(events.EventBootstrap, events.BootstrapPayload{APIAddress: o.APIAddress, Config: c.String(), Logfile: fileName})
|
_, err = bus.Manager.Publish(events.EventBootstrap, events.BootstrapPayload{APIAddress: o.APIAddress, Config: c.String(), Logfile: fileName})
|
||||||
|
@@ -14,10 +14,13 @@ type BrandingText struct {
|
|||||||
Reset string `yaml:"reset"`
|
Reset string `yaml:"reset"`
|
||||||
Recovery string `yaml:"recovery"`
|
Recovery string `yaml:"recovery"`
|
||||||
}
|
}
|
||||||
|
type WebUI struct {
|
||||||
|
Disable bool `yaml:"disable"`
|
||||||
|
ListenAddress string `yaml:"listen_address"`
|
||||||
|
}
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Fast bool `yaml:"fast,omitempty"`
|
Fast bool `yaml:"fast,omitempty"`
|
||||||
|
WebUI WebUI `yaml:"webui"`
|
||||||
Branding BrandingText `yaml:"branding"`
|
Branding BrandingText `yaml:"branding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -228,6 +228,10 @@ func RunInstall(options map[string]string) error {
|
|||||||
c.Install.Reboot = true
|
c.Install.Reboot = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Install.Image != "" {
|
||||||
|
options["system.uri"] = c.Install.Image
|
||||||
|
}
|
||||||
|
|
||||||
env := append(c.Install.Env, c.Env...)
|
env := append(c.Install.Env, c.Env...)
|
||||||
utils.SetEnv(env)
|
utils.SetEnv(env)
|
||||||
|
|
||||||
|
0
internal/webui/public/.keep
Normal file
0
internal/webui/public/.keep
Normal file
134
internal/webui/public/index.html
Normal file
134
internal/webui/public/index.html
Normal file
File diff suppressed because one or more lines are too long
2
internal/webui/public/js/xterm-theme.js
Normal file
2
internal/webui/public/js/xterm-theme.js
Normal file
File diff suppressed because one or more lines are too long
2
internal/webui/public/js/yaml.min.js
vendored
Normal file
2
internal/webui/public/js/yaml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
68
internal/webui/public/message.html
Normal file
68
internal/webui/public/message.html
Normal file
File diff suppressed because one or more lines are too long
119
internal/webui/public/progress.html
Normal file
119
internal/webui/public/progress.html
Normal file
File diff suppressed because one or more lines are too long
241
internal/webui/webui.go
Normal file
241
internal/webui/webui.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kairos-io/kairos/internal/agent"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
process "github.com/mudler/go-processmanager"
|
||||||
|
"github.com/nxadm/tail"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormData struct {
|
||||||
|
CloudConfig string `form:"cloud-config" json:"cloud-config" query:"cloud-config"`
|
||||||
|
Reboot string `form:"reboot" json:"reboot" query:"reboot"`
|
||||||
|
|
||||||
|
PowerOff string `form:"power-off" json:"power-off" query:"power-off"`
|
||||||
|
InstallationDevice string `form:"installation-device" json:"installation-device" query:"installation-device"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed public
|
||||||
|
var embededFiles embed.FS
|
||||||
|
|
||||||
|
func getFileSystem() http.FileSystem {
|
||||||
|
fsys, err := fs.Sub(embededFiles, "public")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.FS(fsys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFS() fs.FS {
|
||||||
|
fsys, err := fs.Sub(embededFiles, "public")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fsys
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamProcess(s *state) func(c echo.Context) error {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
consumeError := func(err error) {
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
websocket.Handler(func(ws *websocket.Conn) {
|
||||||
|
defer ws.Close()
|
||||||
|
for {
|
||||||
|
s.Lock()
|
||||||
|
if s.p == nil {
|
||||||
|
// Write
|
||||||
|
err := websocket.Message.Send(ws, "No process!")
|
||||||
|
consumeError(err)
|
||||||
|
s.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
|
if !s.p.IsAlive() {
|
||||||
|
errOut, err := os.ReadFile(s.p.StderrPath())
|
||||||
|
if err == nil {
|
||||||
|
err := websocket.Message.Send(ws, string(errOut))
|
||||||
|
consumeError(err)
|
||||||
|
}
|
||||||
|
out, err := os.ReadFile(s.p.StdoutPath())
|
||||||
|
if err == nil {
|
||||||
|
err = websocket.Message.Send(ws, string(out))
|
||||||
|
consumeError(err)
|
||||||
|
}
|
||||||
|
err = websocket.Message.Send(ws, "Process stopped!")
|
||||||
|
consumeError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := tail.TailFile(s.p.StdoutPath(), tail.Config{Follow: true})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t2, err := tail.TailFile(s.p.StderrPath(), tail.Config{Follow: true})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case line := <-t.Lines:
|
||||||
|
err = websocket.Message.Send(ws, line.Text+"\r\n")
|
||||||
|
consumeError(err)
|
||||||
|
case line := <-t2.Lines:
|
||||||
|
err = websocket.Message.Send(ws, line.Text+"\r\n")
|
||||||
|
consumeError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).ServeHTTP(c.Response(), c.Request())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
p *process.Process
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateRenderer is a custom html/template renderer for Echo framework.
|
||||||
|
type TemplateRenderer struct {
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a template document.
|
||||||
|
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
|
|
||||||
|
// Add global methods if data is a map
|
||||||
|
if viewContext, isMap := data.(map[string]interface{}); isMap {
|
||||||
|
viewContext["reverse"] = c.Echo().Reverse
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(ctx context.Context) error {
|
||||||
|
|
||||||
|
s := state{}
|
||||||
|
listen := ":8080"
|
||||||
|
|
||||||
|
ec := echo.New()
|
||||||
|
assetHandler := http.FileServer(getFileSystem())
|
||||||
|
|
||||||
|
renderer := &TemplateRenderer{
|
||||||
|
templates: template.Must(template.ParseFS(getFS(), "*.html")),
|
||||||
|
}
|
||||||
|
|
||||||
|
ec.Renderer = renderer
|
||||||
|
agentConfig, err := agent.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if agentConfig.WebUI.ListenAddress != "" {
|
||||||
|
listen = agentConfig.WebUI.ListenAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if agentConfig.WebUI.Disable {
|
||||||
|
log.Println("WebUI installer disabled by branding")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ec.GET("/*", echo.WrapHandler(http.StripPrefix("/", assetHandler)))
|
||||||
|
|
||||||
|
ec.POST("/install", func(c echo.Context) error {
|
||||||
|
|
||||||
|
s.Lock()
|
||||||
|
if s.p != nil {
|
||||||
|
status, _ := s.p.ExitCode()
|
||||||
|
if s.p.IsAlive() || status == "0" {
|
||||||
|
s.Unlock()
|
||||||
|
return c.Redirect(http.StatusSeeOther, "progress.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
|
formData := new(FormData)
|
||||||
|
if err := c.Bind(formData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the form data as necessary
|
||||||
|
cloudConfig := formData.CloudConfig
|
||||||
|
reboot := formData.Reboot
|
||||||
|
powerOff := formData.PowerOff
|
||||||
|
installationDevice := formData.InstallationDevice
|
||||||
|
|
||||||
|
args := []string{"manual-install"}
|
||||||
|
|
||||||
|
if powerOff == "on" {
|
||||||
|
args = append(args, "--poweroff")
|
||||||
|
}
|
||||||
|
if reboot == "on" {
|
||||||
|
args = append(args, "--reboot")
|
||||||
|
}
|
||||||
|
args = append(args, "--device", installationDevice)
|
||||||
|
|
||||||
|
// create tempfile to store cloud-config, bail out if we fail as we couldn't go much further
|
||||||
|
file, err := ioutil.TempFile("", "install-webui")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not create tmpfile for cloud-config: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(file.Name(), []byte(cloudConfig), 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not write tmpfile for cloud-config: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, file.Name())
|
||||||
|
|
||||||
|
s.Lock()
|
||||||
|
s.p = process.New(process.WithName("/usr/bin/kairos-agent"), process.WithArgs(args...), process.WithTemporaryStateDir())
|
||||||
|
s.Unlock()
|
||||||
|
err = s.p.Run()
|
||||||
|
if err != nil {
|
||||||
|
return c.Render(http.StatusOK, "message.html", map[string]interface{}{
|
||||||
|
"message": err.Error(),
|
||||||
|
"type": "danger",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start install process, lock with sentinel
|
||||||
|
return c.Redirect(http.StatusSeeOther, "progress.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
ec.GET("/ws", streamProcess(&s))
|
||||||
|
|
||||||
|
if err := ec.Start(listen); err != nil && err != http.ErrServerClosed {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
ct, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
err := ec.Shutdown(ct)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("shutdown failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Reference in New Issue
Block a user