From a3aadbbaa970c55cbcf93d9fc2bf49d51a6a2fe3 Mon Sep 17 00:00:00 2001 From: Itxaka Date: Thu, 10 Oct 2024 14:18:59 +0200 Subject: [PATCH] Allow installing with no users (#574) --- internal/agent/install.go | 25 +++++++ internal/agent/interactive_install.go | 96 ++++++++++++++++----------- pkg/config/config.go | 1 + pkg/config/config_test.go | 2 +- 4 files changed, 86 insertions(+), 38 deletions(-) diff --git a/internal/agent/install.go b/internal/agent/install.go index 94a259bc..b8aab03e 100644 --- a/internal/agent/install.go +++ b/internal/agent/install.go @@ -29,7 +29,9 @@ import ( "github.com/kairos-io/kairos-sdk/utils" qr "github.com/mudler/go-nodepair/qrcode" "github.com/mudler/go-pluggable" + yip "github.com/mudler/yip/pkg/schema" "github.com/pterm/pterm" + "github.com/twpayne/go-vfs/v4" ) func displayInfo(agentConfig *Config) { @@ -215,6 +217,29 @@ func RunInstall(c *config.Config) error { utils.SetEnv(c.Env) utils.SetEnv(c.Install.Env) + // If nousers is enabled we do not check for the validity of the users and such + // At this point, the config should be fully parsed and the yip stages ready + if !c.Install.NoUsers { + found := false + cc, _ := c.Config.String() + yamlConfig, err := yip.Load(cc, vfs.OSFS, nil, nil) + if err != nil { + return err + } + for _, stage := range yamlConfig.Stages { + for _, x := range stage { + if len(x.Users) > 0 { + found = true + break + } + } + + } + if !found { + return fmt.Errorf("No users found in any stage\nWe require at least one user or the install option 'install.nousers: true' to be set in the config in order to allow installing a system with no users.") + } + } + // UKI path. Check if we are on UKI AND if we are running off a cd, otherwise it makes no sense to run the install // From the installed system if internalutils.IsUkiWithFs(c.Fs) { diff --git a/internal/agent/interactive_install.go b/internal/agent/interactive_install.go index ab8ab40a..9cdb9f29 100644 --- a/internal/agent/interactive_install.go +++ b/internal/agent/interactive_install.go @@ -152,31 +152,45 @@ func InteractiveInstall(debug, spawnShell bool, sourceImgURL string) error { return err } - userName, err := prompt("User to setup", "kairos", canBeEmpty, true, false) + 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 } - userPassword, err := prompt("Password", "", canBeEmpty, true, true) - if err != nil { - return err - } + var userName, userPassword, sshKeys, makeAdmin string - if userPassword == "" { - userPassword = "!" - } + if isYes(createUser) { + userName, err = prompt("User to setup", "kairos", canBeEmpty, true, false) + if err != nil { + return err + } - users, err := prompt("SSH access (rsakey, github/gitlab supported, comma-separated)", "github:someuser,github:someuser2", canBeEmpty, true, false) - if err != nil { - return err - } + userPassword, err = prompt("Password", "", canBeEmpty, true, true) + 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, ",") + 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 @@ -216,41 +230,49 @@ func InteractiveInstall(debug, spawnShell bool, sourceImgURL string) error { return InteractiveInstall(debug, spawnShell, sourceImgURL) } - usersToSet := map[string]schema.User{} + // This is temporal to generate a valid cc file, no need to properly initialize everything + cc := &config.Config{ + Install: &config.Install{ + Device: device, + }, + } - stage := config.NetworkStage.String() + 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: []string{"admin"}, + 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() } - usersToSet = map[string]schema.User{ - userName: user, - } + 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 } - cloudConfig := schema.YipConfig{Name: "Config generated by the installer", - Stages: map[string][]schema.Stage{stage: { - { - Users: usersToSet, - }, - }}} - - // This is temporal to generate a valid cc file, no need to properly initialize everything - cc := &config.Config{ - Install: &config.Install{ - Device: device, - }, - } // Merge all yamls into one dat, err := config.MergeYAML(cloudConfig, cc, result) if err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index beb213b3..8e60d555 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -55,6 +55,7 @@ type Install struct { ExtraPartitions sdkTypes.PartitionList `yaml:"extra-partitions,omitempty" mapstructure:"extra-partitions"` ExtraDirsRootfs []string `yaml:"extra-dirs-rootfs,omitempty" mapstructure:"extra-dirs-rootfs"` Force bool `yaml:"force,omitempty" mapstructure:"force"` + NoUsers bool `yaml:"nousers,omitempty" mapstructure:"nousers"` } func NewConfig(opts ...GenericOptions) *Config { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index a0fcf6ee..cb9d9db1 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -93,7 +93,7 @@ func structFieldsContainedInOtherStruct(left, right interface{}) { leftFieldName := leftTypes.Field(i).Name if leftTypes.Field(i).IsExported() { It(fmt.Sprintf("Checks that the new schema contians the field %s", leftFieldName), func() { - if leftFieldName == "Source" { + if leftFieldName == "Source" || leftFieldName == "NoUsers" { Skip("Schema not updated yet") } Expect(