kairos-agent/internal/agent/interactive_install.go
2023-07-10 14:39:48 +02:00

302 lines
6.4 KiB
Go

package agent
import (
"encoding/json"
"fmt"
"strings"
"github.com/kairos-io/kairos-agent/v2/internal/bus"
"github.com/kairos-io/kairos-agent/v2/internal/cmd"
"github.com/kairos-io/kairos-agent/v2/pkg/config"
"github.com/kairos-io/kairos-agent/v2/pkg/elementalConfig"
events "github.com/kairos-io/kairos-sdk/bus"
"github.com/kairos-io/kairos-sdk/unstructured"
"github.com/erikgeiser/promptkit/textinput"
"github.com/jaypipes/ghw"
"github.com/kairos-io/kairos-sdk/utils"
"github.com/mudler/go-pluggable"
"github.com/mudler/yip/pkg/schema"
"github.com/pterm/pterm"
"github.com/spf13/viper"
)
const (
canBeEmpty = "Unset"
yesNo = "[y]es/[N]o"
)
func prompt(prompt, initialValue, placeHolder string, canBeEmpty, hidden bool) (string, error) {
input := textinput.New(prompt)
input.InitialValue = initialValue
input.Placeholder = placeHolder
if canBeEmpty {
input.Validate = func(s string) error { return nil }
}
input.Hidden = hidden
return input.RunPrompt()
}
func isYes(s string) bool {
i := strings.ToLower(s)
if i == "y" || i == "yes" {
return true
}
return false
}
const (
_ = 1 << (10 * iota)
KiB
MiB
GiB
TiB
)
func promptBool(p events.YAMLPrompt) (string, error) {
def := "n"
if p.Default != "" {
def = p.Default
}
val, err := prompt(p.Prompt, def, yesNo, true, false)
if err != nil {
return "", err
}
if isYes(val) {
val = "true"
} else {
val = "false"
}
return val, nil
}
func promptText(p events.YAMLPrompt) (string, error) {
def := ""
if p.Default != "" {
def = p.Default
}
return prompt(p.Prompt, def, p.PlaceHolder, true, false)
}
func promptToUnstructured(p events.YAMLPrompt, unstructuredYAML map[string]interface{}) (map[string]interface{}, error) {
var res string
if p.AskFirst {
ask, err := prompt(p.AskPrompt, "n", yesNo, true, false)
if err == nil && !isYes(ask) {
return unstructuredYAML, nil
}
}
if p.Bool {
val, err := promptBool(p)
if err != nil {
return unstructuredYAML, err
}
unstructuredYAML[p.YAMLSection] = val
res = val
} else {
val, err := promptText(p)
if err != nil {
return unstructuredYAML, err
}
unstructuredYAML[p.YAMLSection] = val
res = val
}
if res == "" && p.IfEmpty != "" {
res = p.IfEmpty
unstructuredYAML[p.YAMLSection] = res
}
return unstructuredYAML, nil
}
func detectDevice() string {
preferedDevice := "/dev/sda"
maxSize := float64(0)
block, err := ghw.Block()
if err == nil {
for _, disk := range block.Disks {
size := float64(disk.SizeBytes) / float64(GiB)
if size > maxSize {
maxSize = size
preferedDevice = "/dev/" + disk.Name
}
}
}
return preferedDevice
}
func InteractiveInstall(debug, spawnShell bool) error {
var sshUsers []string
bus.Manager.Initialize()
cmd.PrintBranding(DefaultBanner)
agentConfig, err := LoadConfig()
if err != nil {
return err
}
cmd.PrintText(agentConfig.Branding.InteractiveInstall, "Installation")
disks := []string{}
maxSize := float64(0)
preferedDevice := "/dev/sda"
block, err := ghw.Block()
if err == nil {
for _, disk := range block.Disks {
size := float64(disk.SizeBytes) / float64(GiB)
if size > maxSize {
maxSize = size
preferedDevice = "/dev/" + disk.Name
}
disks = append(disks, fmt.Sprintf("/dev/%s: %s (%.2f GiB) ", disk.Name, disk.Model, float64(disk.SizeBytes)/float64(GiB)))
}
}
pterm.Info.Println("Available Disks:")
for _, d := range disks {
pterm.Info.Println(" " + d)
}
device, err := prompt("What's the target install device?", preferedDevice, "Cannot be empty", false, false)
if err != nil {
return err
}
userName, err := prompt("User to setup", "kairos", canBeEmpty, true, false)
if err != nil {
return err
}
userPassword, err := prompt("Password", "", canBeEmpty, true, true)
if err != nil {
return err
}
if userPassword == "" {
userPassword = "!"
}
users, err := prompt("SSH access (rsakey, github/gitlab supported, comma-separated)", "github:someuser,github:someuser2", canBeEmpty, true, false)
if err != nil {
return err
}
// Cleanup the users if we selected the default values as they are not valid users
if users == "github:someuser,github:someuser2" {
users = ""
}
if users != "" {
sshUsers = strings.Split(users, ",")
}
// Prompt the user by prompts defined by the provider
r := []events.YAMLPrompt{}
bus.Manager.Response(events.EventInteractiveInstall, func(p *pluggable.Plugin, resp *pluggable.EventResponse) {
err := json.Unmarshal([]byte(resp.Data), &r)
if err != nil {
fmt.Println(err)
}
})
_, err = bus.Manager.Publish(events.EventInteractiveInstall, events.EventPayload{})
if err != nil {
return err
}
unstructuredYAML := map[string]interface{}{}
for _, p := range r {
unstructuredYAML, err = promptToUnstructured(p, unstructuredYAML)
if err != nil {
return err
}
}
result, err := unstructured.ToYAMLMap(unstructuredYAML)
if err != nil {
return err
}
allGood, err := prompt("Are settings ok?", "n", yesNo, true, false)
if err != nil {
return err
}
if !isYes(allGood) {
return InteractiveInstall(debug, spawnShell)
}
c := &config.Config{
Install: &config.Install{
Device: device,
},
}
usersToSet := map[string]schema.User{}
stage := config.NetworkStage.String()
if userName != "" {
user := schema.User{
Name: userName,
PasswordHash: userPassword,
Groups: []string{"admin"},
SSHAuthorizedKeys: sshUsers,
}
// If we got no ssh keys, we don't need network, do the user as soon as possible
if len(sshUsers) == 0 {
stage = config.InitramfsStage.String()
}
usersToSet = map[string]schema.User{
userName: user,
}
}
cloudConfig := schema.YipConfig{Name: "Config generated by the installer",
Stages: map[string][]schema.Stage{stage: {
{
Users: usersToSet,
},
}}}
dat, err := config.MergeYAML(cloudConfig, c, result)
if err != nil {
return err
}
finalCloudConfig := config.AddHeader("#cloud-config", string(dat))
pterm.Info.Println("Starting installation")
pterm.Info.Println(finalCloudConfig)
// Set debug from here already, so it's loaded by the ReadConfigRun
viper.Set("debug", debug)
// Load the installation Config from the system
installConfig, err := elementalConfig.ReadConfigRun("/etc/elemental")
if err != nil {
return err
}
err = RunInstall(installConfig, map[string]string{
"device": device,
"cc": finalCloudConfig,
})
if err != nil {
pterm.Error.Println(err.Error())
}
if spawnShell {
return utils.Shell().Run()
}
return err
}