From f33eef91b60a7a51bace5449561e1957af099d37 Mon Sep 17 00:00:00 2001 From: Karen Almog Date: Mon, 23 Nov 2020 18:09:52 +0100 Subject: [PATCH 1/3] Install command: add installConfig to cluster yaml Signed-off-by: Karen Almog --- pkg/apis/v1beta1/cluster.go | 11 +++++++++-- pkg/apis/v1beta1/images.go | 2 +- pkg/apis/v1beta1/system.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 pkg/apis/v1beta1/system.go diff --git a/pkg/apis/v1beta1/cluster.go b/pkg/apis/v1beta1/cluster.go index dd1df3a1596d..f25a5716aaf9 100644 --- a/pkg/apis/v1beta1/cluster.go +++ b/pkg/apis/v1beta1/cluster.go @@ -28,11 +28,12 @@ import ( // ClusterConfig cluster manifest type ClusterConfig struct { APIVersion string `yaml:"apiVersion" validate:"eq=k0s.k0sproject.io/v1beta1"` + Extensions *ClusterExtensions `yaml:"extensions,omitempty"` + Images *ClusterImages `yaml:"images"` + Install *InstallSpec `yaml:"installConfig,omitempty"` Kind string `yaml:"kind" validate:"eq=Cluster"` Metadata *ClusterMeta `yaml:"metadata"` Spec *ClusterSpec `yaml:"spec"` - Extensions *ClusterExtensions `yaml:"extensions"` - Images *ClusterImages `yaml:"images"` Telemetry *ClusterTelemetry `yaml:"telemetry"` } @@ -69,6 +70,11 @@ type SchedulerSpec struct { ExtraArgs map[string]string `yaml:"extraArgs"` } +// InstallSpec defines the required fields for the `k0s install` command +type InstallSpec struct { + SystemUsers *SystemUser `yaml:"users,omitempty"` +} + // Validate validates cluster config func (c *ClusterConfig) Validate() []error { var errors []error @@ -118,6 +124,7 @@ func DefaultClusterConfig() *ClusterConfig { Metadata: &ClusterMeta{ Name: "k0s", }, + Install: DefaultInstallSpec(), Spec: DefaultClusterSpec(), Images: DefaultClusterImages(), Telemetry: DefaultClusterTelemetry(), diff --git a/pkg/apis/v1beta1/images.go b/pkg/apis/v1beta1/images.go index 4ce3f9ac0f4e..137cbf832e63 100644 --- a/pkg/apis/v1beta1/images.go +++ b/pkg/apis/v1beta1/images.go @@ -42,7 +42,7 @@ type ClusterImages struct { Calico CalicoImageSpec `yaml:"calico"` - Repository string `yaml:"repository"` + Repository string `yaml:"repository,omitempty"` } func (ci *ClusterImages) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/apis/v1beta1/system.go b/pkg/apis/v1beta1/system.go new file mode 100644 index 000000000000..79e369e03456 --- /dev/null +++ b/pkg/apis/v1beta1/system.go @@ -0,0 +1,32 @@ +package v1beta1 + +import "github.com/k0sproject/k0s/pkg/constant" + +// SystemUser defines the user to use for each component +type SystemUser struct { + ControllerManager string `yaml:"kubeControllerUser,omitempty"` + Etcd string `yaml:"etcdUser,omitempty"` + Kine string `yaml:"kineUser,omitempty"` + Konnectivity string `yaml:"konnectivityUser,omitempty"` + KubeAPIServer string `yaml:"kubeAPIserverUser,omitempty"` + KubeScheduler string `yaml:"kubeSchedulerUser,omitempty"` +} + +// DefaultSystemUsers returns the default system users to be used for the different components +func DefaultSystemUsers() *SystemUser { + return &SystemUser{ + ControllerManager: constant.ControllerManagerUser, + Etcd: constant.EtcdUser, + Kine: constant.KineUser, + Konnectivity: constant.KonnectivityServerUser, + KubeAPIServer: constant.ApiserverUser, + KubeScheduler: constant.SchedulerUser, + } +} + +// DefaultInstallSpec ... +func DefaultInstallSpec() *InstallSpec { + return &InstallSpec{ + SystemUsers: DefaultSystemUsers(), + } +} From c981751c5429b32de3ef7e28541a596f4473a5f0 Mon Sep 17 00:00:00 2001 From: Karen Almog Date: Tue, 1 Dec 2020 15:55:45 +0100 Subject: [PATCH 2/3] Install cmd: add to smoke tests Signed-off-by: Karen Almog --- inttest/common/footloosesuite.go | 9 +++++++++ inttest/footloose-alpine/Dockerfile | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/inttest/common/footloosesuite.go b/inttest/common/footloosesuite.go index d09a1d58b6d2..3d92c153dd29 100644 --- a/inttest/common/footloosesuite.go +++ b/inttest/common/footloosesuite.go @@ -202,11 +202,20 @@ func (s *FootlooseSuite) InitMainController(cfgPath string, dataDir string) erro defer ssh.Disconnect() var startCmd string + var installCmd string + if dataDir != "" { + installCmd = fmt.Sprintf("ETCD_UNSUPPORTED_ARCH=arm64 k0s --debug install --data-dir=%s --config=%s", dataDir, cfgPath) startCmd = fmt.Sprintf("ETCD_UNSUPPORTED_ARCH=arm64 nohup k0s --debug server --data-dir=%s --config=%s >/tmp/k0s-server.log 2>&1 &", dataDir, cfgPath) } else { + installCmd = fmt.Sprintf("ETCD_UNSUPPORTED_ARCH=arm64 k0s --debug install --config=%s", cfgPath) startCmd = fmt.Sprintf("ETCD_UNSUPPORTED_ARCH=arm64 nohup k0s --debug server --config=%s >/tmp/k0s-server.log 2>&1 &", cfgPath) } + _, err = ssh.ExecWithOutput(installCmd) + if err != nil { + return err + } + _, err = ssh.ExecWithOutput(startCmd) if err != nil { return err diff --git a/inttest/footloose-alpine/Dockerfile b/inttest/footloose-alpine/Dockerfile index d7117629f4c7..9bea9d686ca7 100644 --- a/inttest/footloose-alpine/Dockerfile +++ b/inttest/footloose-alpine/Dockerfile @@ -18,10 +18,6 @@ RUN echo "#!/bin/sh" > /etc/local.d/machine-id.start \ && echo "fi" >> /etc/local.d/machine-id.start \ && chmod +x /etc/local.d/machine-id.start -RUN for i in etcd kube-apiserver kube-controller-manager kube-scheduler konnectivity-server; do \ - adduser -H -S -s /sbin/nologin $i; \ - done - # Put kubectl into place to ease up debugging RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.19.0/bin/linux/amd64/kubectl \ && chmod +x ./kubectl \ From a93e731e3652f53123cd224c01064dd78b7f811c Mon Sep 17 00:00:00 2001 From: Karen Almog Date: Tue, 1 Dec 2020 10:24:32 +0100 Subject: [PATCH 3/3] Install cmd: add user setup + cobra cmd Signed-off-by: Karen Almog --- .gitignore | 1 + cmd/helpers.go | 4 +- cmd/install.go | 83 ++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + cmd/token.go | 2 +- cmd/user.go | 5 ++- internal/util/file.go | 15 +++++++- internal/util/users.go | 23 +++++++++++ pkg/install/users.go | 87 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 cmd/install.go create mode 100644 pkg/install/users.go diff --git a/.gitignore b/.gitignore index 2248aed001c2..2ad7a9d4545d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ bindata .*.stamp .tmp .terraform +.idea *.tfstate* aws_private.pem out.json diff --git a/cmd/helpers.go b/cmd/helpers.go index 6eb6af260e08..c1fca5215c4b 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -44,6 +44,8 @@ func ConfigFromYaml(cfgPath string) (*config.ClusterConfig, error) { if clusterConfig.Spec.Storage.Type == config.KineStorageType && clusterConfig.Spec.Storage.Kine == nil { clusterConfig.Spec.Storage.Kine = config.DefaultKineConfig(k0sVars.DataDir) } - + if clusterConfig.Install == nil { + clusterConfig.Install = config.DefaultInstallSpec() + } return clusterConfig, nil } diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 000000000000..7d3453cd08a8 --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,83 @@ +package cmd + +import ( + "fmt" + "os" + "reflect" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/k0sproject/k0s/pkg/apis/v1beta1" + "github.com/k0sproject/k0s/pkg/install" +) + +func init() { + installCmd.Flags().StringVar(&role, "role", "server", "node role (possible values: server or worker. In a single-node setup, a worker role should be used)") +} + +var ( + role string + + installCmd = &cobra.Command{ + Use: "install", + Short: "Helper command for setting up k0s on a brand-new system. Must be run as root (or with sudo)", + RunE: func(cmd *cobra.Command, args []string) error { + switch role { + case "server", "worker": + return setup() + default: + logrus.Errorf("invalid value %s for install role", role) + return cmd.Help() + } + }, + } +) + +// the setup functions: +// * Ensures that the proper users are created +// * sets up startup and logging for k0s +func setup() error { + if os.Geteuid() != 0 { + logrus.Fatal("this command must be run as root!") + } + + if role == "server" { + if err := createServerUsers(); err != nil { + logrus.Errorf("failed to create server users: %v", err) + } + } + + return nil +} + +func createServerUsers() error { + clusterConfig, err := ConfigFromYaml(cfgFile) + if err != nil { + return err + } + users := getUserList(*clusterConfig.Install.SystemUsers) + + var messages []string + for _, v := range users { + if err := install.EnsureUser(v, k0sVars.DataDir); err != nil { + messages = append(messages, err.Error()) + } + } + + if len(messages) > 0 { + return fmt.Errorf(strings.Join(messages, "\n")) + } + return nil +} + +func getUserList(sysUsers v1beta1.SystemUser) []string { + v := reflect.ValueOf(sysUsers) + values := make([]string, v.NumField()) + + for i := 0; i < v.NumField(); i++ { + values[i] = v.Field(i).String() + } + return values +} diff --git a/cmd/root.go b/cmd/root.go index 979460a4a2af..7e7f7bc21ce3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -57,6 +57,7 @@ func init() { rootCmd.AddCommand(etcdCmd) rootCmd.AddCommand(docs) rootCmd.AddCommand(userCmd) + rootCmd.AddCommand(installCmd) longDesc = "k0s - The zero friction Kubernetes - https://k0sproject.io" if build.EulaNotice != "" { diff --git a/cmd/token.go b/cmd/token.go index 957cdac2bd83..3979e97e826e 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -79,7 +79,7 @@ users: Use: "token", Short: "Manage join tokens", RunE: func(cmd *cobra.Command, args []string) error { - return nil + return tokenCreateCmd.Usage() }, } diff --git a/cmd/user.go b/cmd/user.go index de70951cd714..df7119af7ae3 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -49,10 +49,11 @@ users: // userCmd creates new certs and kubeConfig for a user userCmd = &cobra.Command{ - Use: "user", + Use: "user [command]", Short: "Manage user access", RunE: func(cmd *cobra.Command, args []string) error { - return nil + // user command does nothing + return userCreateCmd.Usage() }, } diff --git a/internal/util/file.go b/internal/util/file.go index 8e0e154548b0..a91c0b5433c7 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -18,12 +18,13 @@ package util import ( "fmt" "os" + "os/exec" ) // FileExists checks if a file exists and is not a directory before we // try using it to prevent further errors. -func FileExists(filename string) bool { - info, err := os.Stat(filename) +func FileExists(fileName string) bool { + info, err := os.Stat(fileName) if os.IsNotExist(err) { return false } @@ -42,3 +43,13 @@ func CheckPathPermissions(path string, perm os.FileMode) error { } return nil } + +// Find the path for a given file (similar to `which`) +func GetExecPath(fileName string) (*string, error) { + path, err := exec.LookPath(fileName) + if err != nil { + return nil, err + } + + return &path, nil +} diff --git a/internal/util/users.go b/internal/util/users.go index 42e68ff1bc74..22ee255aa154 100644 --- a/internal/util/users.go +++ b/internal/util/users.go @@ -37,3 +37,26 @@ func GetGID(name string) (int, error) { } return strconv.Atoi(entry.Gid) } + +func CheckIfUserExists(name string) (bool, error) { + _, err := user.Lookup(name) + if _, ok := err.(user.UnknownUserError); ok { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +/* +func GetLinuxDist() (string, error) { + if runtime.GOOS == "windows" { + return "", fmt.Errorf("unsupported OS") + } + cfg, err := ini.Load("/etc/os-release") + if err != nil { + fmt.Printf("failed to read file: %v", err) + } + return cfg.Section("").Key("ID").String(), nil +}*/ diff --git a/pkg/install/users.go b/pkg/install/users.go new file mode 100644 index 000000000000..1a39f75e0210 --- /dev/null +++ b/pkg/install/users.go @@ -0,0 +1,87 @@ +package install + +import ( + "fmt" + "os" + "os/exec" + "os/user" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/k0sproject/k0s/internal/util" +) + +// EnsureUser checks if a user exists, and creates it, if it doesn't +// TODO: we should also consider modifying the user, if the user exists, but with wrong settings +func EnsureUser(name string, homeDir string) error { + shell, err := util.GetExecPath("nologin") + if err != nil { + return err + } + + exists, err := util.CheckIfUserExists(name) + // User doesn't exist + if !exists && err == nil { + // Create the User + if err := CreateUser(name, homeDir, *shell); err != nil { + return err + } + // User perhaps exists, but cannot be fetched + } else if err != nil { + return err + } + // verify that user can be fetched, and exists + _, err = user.Lookup(name) + if err != nil { + return err + } + return nil +} + +// CreateUser creates a system user with either `adduser` or `useradd` command +func CreateUser(userName string, homeDir string, shell string) error { + var userCmd string + var userCmdArgs []string + + logrus.Infof("creating user: %s", userName) + _, err := util.GetExecPath("useradd") + if err == nil { + userCmd = "useradd" + userCmdArgs = []string{`--home`, homeDir, `--shell`, shell, `--system`, `--no-create-home`, userName} + } else { + userCmd = "adduser" + userCmdArgs = []string{`--disabled-password`, `--gecos`, `""`, `--home`, homeDir, `--shell`, shell, `--system`, `--no-create-home`, userName} + } + + cmd := exec.Command(userCmd, userCmdArgs...) + if err := execCmd(cmd); err != nil { + return err + } + return nil +} + +// cmd wrapper +func execCmd(cmd *exec.Cmd) error { + logrus.Debugf("executing command: %v", quoteCmd(cmd)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run command %s: %v", quoteCmd(cmd), err) + } + return nil +} + +// parse a cmd struct to string +func quoteCmd(cmd *exec.Cmd) string { + if len(cmd.Args) == 0 { + return fmt.Sprintf("%q", cmd.Path) + } + + var q []string + for _, s := range cmd.Args { + q = append(q, fmt.Sprintf("%q", s)) + } + return strings.Join(q, ` `) +}