mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-20 22:21:13 +02:00
Allows configuring: - cidr. - dhcp enable/disable. - MTU. - Ignore. - Dhcp metric. Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
536 lines
11 KiB
Go
536 lines
11 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
// Package installer contains terminal UI based talos interactive installer parts.
|
|
package installer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
|
|
"github.com/talos-systems/talos/internal/pkg/tui/components"
|
|
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
|
|
clientconfig "github.com/talos-systems/talos/pkg/machinery/client/config"
|
|
"github.com/talos-systems/talos/pkg/version"
|
|
)
|
|
|
|
// NewPage creates a new installer page.
|
|
func NewPage(name string, items ...*components.Item) *Page {
|
|
return &Page{
|
|
name: name,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
// Page represents a single installer page.
|
|
type Page struct {
|
|
name string
|
|
items []*components.Item
|
|
}
|
|
|
|
// Installer interactive installer text based UI.
|
|
type Installer struct {
|
|
pages *tview.Pages
|
|
app *tview.Application
|
|
wg sync.WaitGroup
|
|
err error
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
addedPages map[string]bool
|
|
state *State
|
|
}
|
|
|
|
// NewInstaller creates a new text based installer.
|
|
func NewInstaller() *Installer {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &Installer{
|
|
pages: tview.NewPages(),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
const (
|
|
color = tcell.Color238
|
|
frameBGColor = tcell.Color235
|
|
inactiveColor = tcell.Color236
|
|
)
|
|
|
|
var spinner = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
|
|
const (
|
|
phaseInit = iota
|
|
phaseConfigure
|
|
phaseApply
|
|
)
|
|
|
|
// Run starts interactive installer.
|
|
func (installer *Installer) Run(conn *Connection) error {
|
|
installer.startApp()
|
|
defer installer.stopApp()
|
|
|
|
var (
|
|
err error
|
|
description string
|
|
)
|
|
|
|
for phase := phaseInit; phase <= phaseApply; {
|
|
switch phase {
|
|
case phaseInit:
|
|
description = "get the node information"
|
|
err = installer.init(conn)
|
|
case phaseConfigure:
|
|
description = "generate the configuration"
|
|
err = installer.configure()
|
|
case phaseApply:
|
|
description = "apply the configuration"
|
|
err = installer.apply(conn)
|
|
}
|
|
|
|
if err != nil && err != context.Canceled {
|
|
choice := installer.showModal(
|
|
fmt.Sprintf("Failed to %s", description),
|
|
err.Error(),
|
|
"Quit", "Retry",
|
|
)
|
|
|
|
if choice == 1 {
|
|
// apply should be retried from configure
|
|
if phase == phaseApply {
|
|
phase = phaseConfigure
|
|
}
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
phase++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (installer *Installer) startApp() {
|
|
if installer.app != nil {
|
|
return
|
|
}
|
|
|
|
installer.wg.Add(1)
|
|
installer.app = tview.NewApplication()
|
|
|
|
go func() {
|
|
defer installer.wg.Done()
|
|
defer installer.cancel()
|
|
|
|
if err := installer.app.SetRoot(installer.pages, true).EnableMouse(true).Run(); err != nil {
|
|
installer.err = err
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (installer *Installer) stopApp() {
|
|
if installer.app == nil {
|
|
return
|
|
}
|
|
|
|
installer.app.Stop()
|
|
installer.wg.Wait()
|
|
installer.app = nil
|
|
}
|
|
|
|
func (installer *Installer) init(conn *Connection) (err error) {
|
|
installer.startApp()
|
|
|
|
s := components.NewSpinner(
|
|
fmt.Sprintf("Connecting to the maintenance service at [green::]%s[white::]", conn.nodeEndpoint),
|
|
spinner,
|
|
installer.app,
|
|
)
|
|
|
|
s.SetBackgroundColor(color)
|
|
installer.addPage("Gathering the node information", s, true, nil)
|
|
|
|
installer.state, err = NewState(
|
|
installer.ctx,
|
|
installer,
|
|
conn,
|
|
)
|
|
|
|
select {
|
|
case <-s.Stop(err == nil):
|
|
case <-installer.ctx.Done():
|
|
return context.Canceled
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// nolint:gocyclo
|
|
func (installer *Installer) configure() error {
|
|
var (
|
|
err error
|
|
forms []*components.Form
|
|
)
|
|
|
|
currentPage := 0
|
|
menuButtons := []*components.MenuButton{}
|
|
|
|
done := make(chan struct{})
|
|
state := installer.state
|
|
|
|
setPage := func(index int) {
|
|
if index < 0 || index >= len(state.pages) {
|
|
return
|
|
}
|
|
|
|
menuButtons[currentPage].SetActive(false)
|
|
currentPage = index
|
|
menuButtons[currentPage].SetActive(true)
|
|
|
|
installer.pages.SwitchToPage(state.pages[currentPage].name)
|
|
installer.app.SetFocus(forms[currentPage])
|
|
}
|
|
|
|
capture := installer.app.GetInputCapture()
|
|
installer.app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
//nolint:exhaustive
|
|
switch e.Key() {
|
|
case tcell.KeyCtrlN:
|
|
setPage(currentPage + 1)
|
|
case tcell.KeyCtrlB:
|
|
setPage(currentPage - 1)
|
|
}
|
|
|
|
// page jump by ctrl/alt + N
|
|
if e.Rune() >= '1' && e.Rune() < '9' {
|
|
if e.Modifiers()&(tcell.ModAlt|tcell.ModCtrl) != 0 {
|
|
setPage(int(e.Rune()) - 49)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if capture != nil {
|
|
return capture(e)
|
|
}
|
|
|
|
return e
|
|
})
|
|
|
|
defer installer.app.SetInputCapture(capture)
|
|
|
|
menu := tview.NewFlex()
|
|
menu.SetBackgroundColor(frameBGColor)
|
|
|
|
addMenuItem := func(name string, index int) {
|
|
button := components.NewMenuButton(name)
|
|
button.SetActiveColors(color, tcell.ColorIvory)
|
|
button.SetInactiveColors(inactiveColor, tcell.ColorIvory)
|
|
|
|
func(page int) {
|
|
button.SetSelectedFunc(func() {
|
|
setPage(page)
|
|
})
|
|
}(index)
|
|
|
|
menu.AddItem(button, len(name)+4, 1, false)
|
|
menuButtons = append(menuButtons, button)
|
|
|
|
if currentPage == index {
|
|
button.SetActive(true)
|
|
}
|
|
}
|
|
|
|
forms = make([]*components.Form, len(state.pages))
|
|
|
|
for i, p := range state.pages {
|
|
err = func(index int) error {
|
|
form := components.NewForm(installer.app)
|
|
form.SetBackgroundColor(color)
|
|
forms[i] = form
|
|
|
|
if e := form.AddFormItems(p.items); e != nil {
|
|
return e
|
|
}
|
|
|
|
if index > 0 {
|
|
back := form.AddMenuButton("[::u]B[::-]ack", false)
|
|
back.SetSelectedFunc(func() {
|
|
setPage(index - 1)
|
|
})
|
|
}
|
|
|
|
addMenuItem(p.name, index)
|
|
|
|
form.SetBackgroundColor(color)
|
|
|
|
if index < len(state.pages)-1 {
|
|
next := form.AddMenuButton("[::u]N[::-]ext", index == 0)
|
|
next.SetSelectedFunc(func() {
|
|
setPage(index + 1)
|
|
})
|
|
} else {
|
|
install := form.AddMenuButton("Install", false)
|
|
install.SetBackgroundColor(tcell.ColorGreen)
|
|
install.SetSelectedFunc(func() {
|
|
close(done)
|
|
})
|
|
}
|
|
|
|
installer.addPage(p.name, form, index == 0, menu)
|
|
|
|
return nil
|
|
}(i)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
setPage(0)
|
|
|
|
select {
|
|
case <-installer.ctx.Done():
|
|
return context.Canceled
|
|
case <-done: // nothing here, just waiting
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (installer *Installer) apply(conn *Connection) error {
|
|
var (
|
|
config []byte
|
|
talosconfig *clientconfig.Config
|
|
err error
|
|
response *machineapi.GenerateConfigurationResponse
|
|
)
|
|
|
|
list := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
list.SetBackgroundColor(color)
|
|
installer.addPage("Installing Talos", list, true, nil)
|
|
|
|
{
|
|
s := components.NewSpinner(
|
|
"Generating configuration...",
|
|
spinner,
|
|
installer.app,
|
|
)
|
|
s.SetBackgroundColor(color)
|
|
|
|
list.AddItem(s, 1, 1, false)
|
|
|
|
response, err = installer.state.GenConfig()
|
|
|
|
s.Stop(err == nil)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config = response.Data[0]
|
|
talosconfig, err = clientconfig.FromBytes(response.Talosconfig)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
{
|
|
s := components.NewSpinner(
|
|
"Applying configuration...",
|
|
spinner,
|
|
installer.app,
|
|
)
|
|
s.SetBackgroundColor(color)
|
|
|
|
// TODO: progress bar, logs?
|
|
list.AddItem(s, 1, 1, false)
|
|
_, err = conn.ApplyConfiguration(&machineapi.ApplyConfigurationRequest{
|
|
Data: config,
|
|
})
|
|
|
|
s.Stop(err == nil)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return installer.writeTalosconfig(list, talosconfig)
|
|
}
|
|
|
|
func (installer *Installer) writeTalosconfig(list *tview.Flex, talosconfig *clientconfig.Config) error {
|
|
path, err := clientconfig.GetDefaultPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
|
|
var config *clientconfig.Config
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
if err == nil {
|
|
config, err = clientconfig.ReadFrom(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
text := tview.NewTextView()
|
|
addLines := func(lines ...string) {
|
|
t := text.GetText(false)
|
|
t += strings.Join(lines, "\n")
|
|
text.SetText(t)
|
|
installer.app.Draw()
|
|
}
|
|
|
|
addLines(
|
|
"",
|
|
fmt.Sprintf("Merging talosconfig into %s...", path),
|
|
)
|
|
text.SetBackgroundColor(color)
|
|
list.AddItem(text, 0, 1, false)
|
|
|
|
renames := []clientconfig.Rename{}
|
|
if config != nil {
|
|
renames = config.Merge(talosconfig)
|
|
} else {
|
|
config = talosconfig
|
|
}
|
|
|
|
for _, rename := range renames {
|
|
addLines(fmt.Sprintf("Renamed %s.", rename.String()))
|
|
}
|
|
|
|
context := talosconfig.Context
|
|
if len(renames) != 0 {
|
|
context = renames[0].To
|
|
}
|
|
|
|
config.Context = context
|
|
addLines(fmt.Sprintf("Set current context to %q.", context))
|
|
|
|
err = config.Save(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addLines(
|
|
"",
|
|
"Press any key to exit.",
|
|
)
|
|
|
|
installer.awaitKey()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (installer *Installer) awaitKey(keys ...tcell.Key) {
|
|
done := make(chan struct{})
|
|
|
|
installer.app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
for _, key := range keys {
|
|
if e.Key() == key {
|
|
close(done)
|
|
}
|
|
}
|
|
|
|
if len(keys) == 0 {
|
|
close(done)
|
|
}
|
|
|
|
return e
|
|
})
|
|
|
|
select {
|
|
case <-done:
|
|
case <-installer.ctx.Done():
|
|
}
|
|
}
|
|
|
|
// showModal block execution and show modal window.
|
|
func (installer *Installer) showModal(title, text string, buttons ...string) int {
|
|
done := make(chan struct{})
|
|
|
|
index := -1
|
|
|
|
modal := tview.NewModal().
|
|
SetText(text).
|
|
AddButtons(buttons).
|
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
|
index = buttonIndex
|
|
close(done)
|
|
})
|
|
|
|
installer.addPage(title, modal, true, nil)
|
|
installer.app.SetFocus(modal)
|
|
installer.app.Draw()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-installer.ctx.Done():
|
|
}
|
|
|
|
return index
|
|
}
|
|
|
|
func (installer *Installer) addPage(name string, primitive tview.Primitive, switchToPage bool, menu tview.Primitive) {
|
|
if !installer.addedPages[name] {
|
|
content := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
page := tview.NewFrame(primitive).SetBorders(1, 1, 1, 1, 2, 2)
|
|
page.SetBackgroundColor(color)
|
|
|
|
if menu != nil {
|
|
content.AddItem(menu, 3, 1, false)
|
|
}
|
|
|
|
content.AddItem(page, 0, 1, false)
|
|
|
|
frame := tview.NewFrame(content).SetBorders(1, 1, 1, 1, 2, 2).
|
|
AddText(name, true, tview.AlignLeft, tcell.ColorWhite).
|
|
AddText("Talos Interactive Installer", true, tview.AlignCenter, tcell.ColorWhite).
|
|
AddText(version.Tag, true, tview.AlignRight, tcell.ColorIvory).
|
|
AddText("<CTRL>+B/<CTRL>+N to switch tabs", false, tview.AlignLeft, tcell.ColorIvory).
|
|
AddText("<TAB> for navigation", false, tview.AlignLeft, tcell.ColorIvory).
|
|
AddText("[::b]Key Bindings[::-]", false, tview.AlignLeft, tcell.ColorIvory)
|
|
|
|
frame.SetBackgroundColor(frameBGColor)
|
|
|
|
if switchToPage {
|
|
installer.pages.AddAndSwitchToPage(name, frame, true)
|
|
} else {
|
|
installer.pages.AddPage(name,
|
|
frame, true, false)
|
|
}
|
|
} else if switchToPage {
|
|
installer.pages.SwitchToPage(name)
|
|
}
|
|
|
|
installer.app.ForceDraw()
|
|
}
|