1
0
mirror of https://github.com/kairos-io/kairos-agent.git synced 2025-05-03 22:06:19 +00:00
kairos-agent/internal/agent/interactive_install.go
2024-10-10 14:18:59 +02:00

313 lines
7.4 KiB
Go

package agent
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"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"
events "github.com/kairos-io/kairos-sdk/bus"
"github.com/kairos-io/kairos-sdk/collector"
"github.com/kairos-io/kairos-sdk/ghw"
"github.com/kairos-io/kairos-sdk/unstructured"
"github.com/erikgeiser/promptkit/textinput"
"github.com/kairos-io/kairos-sdk/utils"
"github.com/mudler/go-pluggable"
"github.com/mudler/yip/pkg/schema"
"github.com/pterm/pterm"
)
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 InteractiveInstall(debug, spawnShell bool, sourceImgURL string) 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"
for _, disk := range ghw.GetDisks(ghw.NewPaths(""), nil) {
// skip useless devices (/dev/ram, /dev/loop, /dev/sr, /dev/zram)
if strings.HasPrefix(disk.Name, "loop") || strings.HasPrefix(disk.Name, "ram") || strings.HasPrefix(disk.Name, "sr") || strings.HasPrefix(disk.Name, "zram") {
continue
}
size := float64(disk.SizeBytes) / float64(GiB)
if size > maxSize {
maxSize = size
preferedDevice = "/dev/" + disk.Name
}
disks = append(disks, fmt.Sprintf("/dev/%s: (%.2f GiB) ", disk.Name, 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
}
createUser, err := prompt("Do you want to create any users? If not, system will not be accesible via terminal or ssh", "y", yesNo, true, false)
if err != nil {
return err
}
var userName, userPassword, sshKeys, makeAdmin string
if isYes(createUser) {
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 = "!"
}
sshKeys, err = prompt("SSH access (rsakey, github/gitlab supported, comma-separated)", "github:someuser,github:someuser2", canBeEmpty, true, false)
if err != nil {
return err
}
makeAdmin, err = prompt("Make the user an admin (with sudo permissions)?", "y", yesNo, true, false)
if err != nil {
return err
}
// Cleanup the users if we selected the default values as they are not valid users
if sshKeys == "github:someuser,github:someuser2" {
sshKeys = ""
}
if sshKeys != "" {
sshUsers = strings.Split(sshKeys, ",")
}
}
// 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, sourceImgURL)
}
// This is temporal to generate a valid cc file, no need to properly initialize everything
cc := &config.Config{
Install: &config.Install{
Device: device,
},
}
var cloudConfig schema.YipConfig
// Only add the user stage if we have any users
if userName != "" {
var isAdmin []string
if isYes(makeAdmin) {
isAdmin = append(isAdmin, "admin")
}
user := schema.User{
Name: userName,
PasswordHash: userPassword,
Groups: isAdmin,
SSHAuthorizedKeys: sshUsers,
}
stage := config.NetworkStage.String()
// 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()
}
cloudConfig = schema.YipConfig{Name: "Config generated by the installer",
Stages: map[string][]schema.Stage{stage: {
{
Users: map[string]schema.User{
userName: user,
},
},
}}}
} else {
// If no users, we need to set this option to skip the user validation and confirm that we want a system with no users.
cc.Install.NoUsers = true
}
// Merge all yamls into one
dat, err := config.MergeYAML(cloudConfig, cc, result)
if err != nil {
return err
}
finalCloudConfig := config.AddHeader("#cloud-config", string(dat))
// Store it in a temp file and load it with the collector to have a standard way of loading across all methods
tmpdir, err := os.MkdirTemp("", "kairos-install-")
if err == nil {
err = os.WriteFile(filepath.Join(tmpdir, "kairos-event-install-data.yaml"), []byte(finalCloudConfig), os.ModePerm)
if err != nil {
fmt.Printf("could not write event cloud init: %s\n", err.Error())
}
cliConf := generateInstallConfForCLIArgs(sourceImgURL)
cc, _ = config.Scan(collector.Directories(tmpdir),
collector.Readers(strings.NewReader(cliConf)),
collector.MergeBootLine, collector.NoLogs)
}
pterm.Info.Println("Starting installation")
// Generate final config
ccString, _ := cc.String()
pterm.Info.Println(ccString)
err = RunInstall(cc)
if err != nil {
pterm.Error.Println(err.Error())
}
if spawnShell {
return utils.Shell().Run()
}
return err
}