mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-17 15:27:58 +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 {
|
||||
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})
|
||||
|
@@ -14,10 +14,13 @@ type BrandingText struct {
|
||||
Reset string `yaml:"reset"`
|
||||
Recovery string `yaml:"recovery"`
|
||||
}
|
||||
|
||||
type WebUI struct {
|
||||
Disable bool `yaml:"disable"`
|
||||
ListenAddress string `yaml:"listen_address"`
|
||||
}
|
||||
type Config struct {
|
||||
Fast bool `yaml:"fast,omitempty"`
|
||||
|
||||
Fast bool `yaml:"fast,omitempty"`
|
||||
WebUI WebUI `yaml:"webui"`
|
||||
Branding BrandingText `yaml:"branding"`
|
||||
}
|
||||
|
||||
|
@@ -228,6 +228,10 @@ func RunInstall(options map[string]string) error {
|
||||
c.Install.Reboot = true
|
||||
}
|
||||
|
||||
if c.Install.Image != "" {
|
||||
options["system.uri"] = c.Install.Image
|
||||
}
|
||||
|
||||
env := append(c.Install.Env, c.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