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..35d5344da5 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 @@ -285,18 +286,47 @@ func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error { } // Info returns the provider info. +// The info is cached on the first time of the execution. 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 { + CgroupDriver string `json:"CgroupDriver"` // "systemd", "cgroupfs", "none" + 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", + } + // When CgroupDriver == "none", the MemoryLimit/PidsLimit/CPUShares + // values are meaningless and need to be considered false. + // https://github.com/moby/moby/issues/42151 + if dInfo.CgroupDriver != "none" { + info.SupportsMemoryLimit = dInfo.MemoryLimit + info.SupportsPidsLimit = dInfo.PidsLimit + info.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 93317ffba1..e6ec5bd806 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 @@ -354,9 +356,47 @@ func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error { } // Info returns the provider info. +// The info is cached on the first time of the execution. func (p *provider) Info() (*providers.ProviderInfo, error) { + if p.info == nil { + p.info = info(p.logger) + } + return p.info, nil +} + +func info(logger log.Logger) *providers.ProviderInfo { + 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 + return info } 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 }