From 4d7e6c9b98cffe0a9ed0cd299991e6d2c0d8d992 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 15 Mar 2021 15:05:01 +0900 Subject: [PATCH] rootless: fail early if prerequiresites are not satisfied `kind create cluster` now prints errors before running the containers. Signed-off-by: Akihiro Suda --- pkg/cluster/internal/create/create.go | 21 ++++++++++ .../internal/providers/docker/provider.go | 33 ++++++++++++--- .../internal/providers/podman/provider.go | 42 ++++++++++++++++++- pkg/cluster/internal/providers/provider.go | 6 ++- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/pkg/cluster/internal/create/create.go b/pkg/cluster/internal/create/create.go index 720120737a..e89d789605 100644 --- a/pkg/cluster/internal/create/create.go +++ b/pkg/cluster/internal/create/create.go @@ -66,6 +66,11 @@ type ClusterOptions struct { // Cluster creates a cluster func Cluster(logger log.Logger, p providers.Provider, opts *ClusterOptions) error { + // validate provider first + if err := validateProvider(p); err != nil { + return err + } + // default / process options (namely config) if err := fixupOptions(opts); err != nil { return err @@ -234,3 +239,19 @@ func fixupOptions(opts *ClusterOptions) error { return nil } + +func validateProvider(p providers.Provider) error { + info, err := p.Info() + if err != nil { + return err + } + if info.Rootless { + if !info.Cgroup2 { + return errors.New("running kind with rootless provider requires cgroup v2, see https://kind.sigs.k8s.io/docs/user/rootless/") + } + if !info.SupportsMemoryLimit || !info.SupportsPidsLimit || !info.SupportsCPUShares { + return errors.New("running kind with rootless provider requires setting systemd property \"Delegate=yes\", see https://kind.sigs.k8s.io/docs/user/rootless/") + } + } + return nil +} diff --git a/pkg/cluster/internal/providers/docker/provider.go b/pkg/cluster/internal/providers/docker/provider.go index 680b2181b1..97358114b6 100644 --- a/pkg/cluster/internal/providers/docker/provider.go +++ b/pkg/cluster/internal/providers/docker/provider.go @@ -51,6 +51,7 @@ func NewProvider(logger log.Logger) providers.Provider { // see NewProvider type provider struct { logger log.Logger + info *providers.ProviderInfo } // String implements fmt.Stringer @@ -286,17 +287,39 @@ func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error { // Info returns the provider info. func (p *provider) Info() (*providers.ProviderInfo, error) { - cmd := exec.Command("docker", "info", "--format", "{{json .SecurityOptions}}") + var err error + if p.info == nil { + p.info, err = info() + } + return p.info, err +} + +// dockerInfo corresponds to `docker info --format '{{json .}}'` +type dockerInfo struct { + CgroupVersion string `json:"CgroupVersion"` // e.g. "2" + MemoryLimit bool `json:"MemoryLimit"` + PidsLimit bool `json:"PidsLimit"` + CPUShares bool `json:"CPUShares"` + SecurityOptions []string `json:"SecurityOptions"` +} + +func info() (*providers.ProviderInfo, error) { + cmd := exec.Command("docker", "info", "--format", "{{json .}}") out, err := exec.Output(cmd) if err != nil { return nil, errors.Wrap(err, "failed to get docker info") } - var securityOptions []string - if err := json.Unmarshal(out, &securityOptions); err != nil { + var dInfo dockerInfo + if err := json.Unmarshal(out, &dInfo); err != nil { return nil, err } - var info providers.ProviderInfo - for _, o := range securityOptions { + info := providers.ProviderInfo{ + Cgroup2: dInfo.CgroupVersion == "2", + SupportsMemoryLimit: dInfo.MemoryLimit, + SupportsPidsLimit: dInfo.PidsLimit, + SupportsCPUShares: dInfo.CPUShares, + } + for _, o := range dInfo.SecurityOptions { // o is like "name=seccomp,profile=default", or "name=rootless", csvReader := csv.NewReader(strings.NewReader(o)) sliceSlice, err := csvReader.ReadAll() diff --git a/pkg/cluster/internal/providers/podman/provider.go b/pkg/cluster/internal/providers/podman/provider.go index 2e54e07f73..71bf32b031 100644 --- a/pkg/cluster/internal/providers/podman/provider.go +++ b/pkg/cluster/internal/providers/podman/provider.go @@ -19,6 +19,7 @@ package podman import ( "encoding/json" "fmt" + "io/ioutil" "net" "os" "path/filepath" @@ -53,6 +54,7 @@ func NewProvider(logger log.Logger) providers.Provider { // see NewProvider type provider struct { logger log.Logger + info *providers.ProviderInfo } // String implements fmt.Stringer @@ -347,8 +349,46 @@ func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error { // Info returns the provider info. func (p *provider) Info() (*providers.ProviderInfo, error) { + var err error + if p.info == nil { + p.info, err = info(p.logger) + } + return p.info, err +} + +func info(logger log.Logger) (*providers.ProviderInfo, error) { + euid := os.Geteuid() info := &providers.ProviderInfo{ - Rootless: os.Geteuid() != 0, + Rootless: euid != 0, + } + if _, err := os.Stat("/sys/fs/cgroup/cgroup.controllers"); err == nil { + info.Cgroup2 = true + // Unlike `docker info`, `podman info` does not print available cgroup controllers. + // So we parse "cgroup.subtree_control" file by ourselves. + subtreeControl := "/sys/fs/cgroup/cgroup.subtree_control" + if info.Rootless { + // Change subtreeControl to the path of the systemd user-instance. + // Non-systemd hosts are not supported. + subtreeControl = fmt.Sprintf("/sys/fs/cgroup/user.slice/user-%d.slice/user@%d.service/cgroup.subtree_control", euid, euid) + } + if subtreeControlBytes, err := ioutil.ReadFile(subtreeControl); err != nil { + logger.Warnf("failed to read %q: %+v", subtreeControl, err) + } else { + for _, controllerName := range strings.Fields(string(subtreeControlBytes)) { + switch controllerName { + case "cpu": + info.SupportsCPUShares = true + case "memory": + info.SupportsMemoryLimit = true + case "pids": + info.SupportsPidsLimit = true + } + } + } + } else if !info.Rootless { + info.SupportsCPUShares = true + info.SupportsMemoryLimit = true + info.SupportsPidsLimit = true } return info, nil } diff --git a/pkg/cluster/internal/providers/provider.go b/pkg/cluster/internal/providers/provider.go index 82e3d60408..d2f8e36287 100644 --- a/pkg/cluster/internal/providers/provider.go +++ b/pkg/cluster/internal/providers/provider.go @@ -51,5 +51,9 @@ type Provider interface { // ProviderInfo is the info of the provider type ProviderInfo struct { - Rootless bool + Rootless bool + Cgroup2 bool + SupportsMemoryLimit bool + SupportsPidsLimit bool + SupportsCPUShares bool }