From 7bf30d7bf13852d1f2b493817489d37170d2b8a4 Mon Sep 17 00:00:00 2001
From: Michael Crosby <crosbymichael@gmail.com>
Date: Wed, 6 Nov 2019 11:39:36 -0500
Subject: [PATCH] Rename playground to cgctl

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
---
 .gitignore                     |   2 +-
 cmd/cgctl/main.go              | 120 +++++++++++++++++++++++++++++++++
 cmd/cgroups-playground/main.go |  54 ---------------
 v2/errors.go                   |   3 +-
 v2/manager.go                  |  76 ++++++++++++++++++---
 v2/paths.go                    |  14 ----
 6 files changed, 189 insertions(+), 80 deletions(-)
 create mode 100644 cmd/cgctl/main.go
 delete mode 100644 cmd/cgroups-playground/main.go

diff --git a/.gitignore b/.gitignore
index 23495f5f..54f3c5db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
 example/example
-cmd/cgroups-playground/cgroups-playground
+cmd/cgctl
diff --git a/cmd/cgctl/main.go b/cmd/cgctl/main.go
new file mode 100644
index 00000000..27ceb01c
--- /dev/null
+++ b/cmd/cgctl/main.go
@@ -0,0 +1,120 @@
+/*
+   Copyright The containerd Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"os"
+
+	v2 "github.com/containerd/cgroups/v2"
+	"github.com/sirupsen/logrus"
+	"github.com/urfave/cli"
+)
+
+func main() {
+	app := cli.NewApp()
+	app.Name = "cgctl"
+	app.Version = "1"
+	app.Usage = "cgroup v2 management tool"
+	app.Flags = []cli.Flag{
+		cli.BoolFlag{
+			Name:  "debug",
+			Usage: "enable debug output in the logs",
+		},
+		cli.StringFlag{
+			Name:  "mountpoint",
+			Usage: "cgroup mountpoint",
+			Value: "/sys/fs/cgroup",
+		},
+	}
+	app.Commands = []cli.Command{
+		newCommand,
+		delCommand,
+		listCommand,
+	}
+	app.Before = func(clix *cli.Context) error {
+		if clix.GlobalBool("debug") {
+			logrus.SetLevel(logrus.DebugLevel)
+		}
+		return nil
+	}
+	if err := app.Run(os.Args); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+}
+
+var newCommand = cli.Command{
+	Name:  "new",
+	Usage: "create a new cgroup",
+	Flags: []cli.Flag{
+		cli.BoolFlag{
+			Name:  "enable",
+			Usage: "enable the controllers for the group",
+		},
+	},
+	Action: func(clix *cli.Context) error {
+		path := clix.Args().First()
+		c, err := v2.NewManager(clix.GlobalString("mountpoint"), path, nil)
+		if err != nil {
+			return err
+		}
+		if clix.Bool("enable") {
+			controllers, err := c.ListControllers()
+			if err != nil {
+				return err
+			}
+			if err := c.ToggleControllers(controllers, v2.Enable); err != nil {
+				return err
+			}
+		}
+		return nil
+	},
+}
+
+var delCommand = cli.Command{
+	Name:  "del",
+	Usage: "delete a cgroup",
+	Action: func(clix *cli.Context) error {
+		path := clix.Args().First()
+		c, err := v2.LoadManager(clix.GlobalString("mountpoint"), path)
+		if err != nil {
+			return err
+		}
+		return c.Delete()
+	},
+}
+
+var listCommand = cli.Command{
+	Name:  "list",
+	Usage: "list processes in a cgroup",
+	Action: func(clix *cli.Context) error {
+		path := clix.Args().First()
+		c, err := v2.LoadManager(clix.GlobalString("mountpoint"), path)
+		if err != nil {
+			return err
+		}
+		procs, err := c.Procs(true)
+		if err != nil {
+			return err
+		}
+		for _, p := range procs {
+			fmt.Println(p)
+		}
+		return nil
+	},
+}
diff --git a/cmd/cgroups-playground/main.go b/cmd/cgroups-playground/main.go
deleted file mode 100644
index 641b541d..00000000
--- a/cmd/cgroups-playground/main.go
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-   Copyright The containerd Authors.
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-*/
-
-package main
-
-import (
-	"os"
-
-	v2 "github.com/containerd/cgroups/v2"
-	"github.com/sirupsen/logrus"
-)
-
-func main() {
-	if err := xmain(); err != nil {
-		logrus.Fatalf("%+v", err)
-	}
-}
-
-func xmain() error {
-	pid := os.Getpid()
-	g, err := v2.PidGroupPath(pid)
-	if err != nil {
-		return err
-	}
-	unifiedMountpoint := "/sys/fs/cgroup"
-	logrus.Infof("Loading V2 for %q (PID %d), mountpoint=%q", g, pid, unifiedMountpoint)
-	cg, err := v2.LoadManager(unifiedMountpoint, g)
-	if err != nil {
-		return err
-	}
-	processes, err := cg.Procs(true)
-	if err != nil {
-		return err
-	}
-	logrus.Infof("Has %d processes (recursively)", len(processes))
-	for i, s := range processes {
-		logrus.Infof("Process %d: %d", i, s)
-	}
-
-	return nil
-}
diff --git a/v2/errors.go b/v2/errors.go
index 60af72dd..46d2d9c2 100644
--- a/v2/errors.go
+++ b/v2/errors.go
@@ -31,8 +31,7 @@ var (
 	ErrCPUNotSupported          = errors.New("cgroups: cpu cgroup (v2) not supported on this system")
 	ErrCgroupDeleted            = errors.New("cgroups: cgroup deleted")
 	ErrNoCgroupMountDestination = errors.New("cgroups: cannot find cgroup mount destination")
-
-	ErrInvalidGroupPath = errors.New("cgroups: group path format must be compatible with /proc/PID/cgroup")
+	ErrInvalidGroupPath         = errors.New("cgroups: invalid group path")
 )
 
 // ErrorHandler is a function that handles and acts on errors
diff --git a/v2/manager.go b/v2/manager.go
index cc8397dc..5b6f2d62 100644
--- a/v2/manager.go
+++ b/v2/manager.go
@@ -17,6 +17,7 @@
 package v2
 
 import (
+	"bufio"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -27,6 +28,10 @@ import (
 	"github.com/pkg/errors"
 )
 
+const (
+	subtreeControl = "cgroup.subtree_control"
+)
+
 type cgValuer interface {
 	Values() []Value
 }
@@ -93,18 +98,19 @@ func writeValues(path string, values []Value) error {
 }
 
 func NewManager(mountpoint string, group string, resources *Resources) (*Manager, error) {
-	if err := VerifyGroupPath(group); err != nil {
-		return nil, err
+	if group == "" {
+		return nil, ErrInvalidGroupPath
 	}
-
 	path := filepath.Join(mountpoint, group)
 	if err := os.MkdirAll(path, defaultDirPerm); err != nil {
 		return nil, err
 	}
-	if err := writeValues(path, resources.Values()); err != nil {
-		// clean up cgroup dir on failure
-		os.Remove(path)
-		return nil, err
+	if resources != nil {
+		if err := writeValues(path, resources.Values()); err != nil {
+			// clean up cgroup dir on failure
+			os.Remove(path)
+			return nil, err
+		}
 	}
 	return &Manager{
 		unifiedMountpoint: mountpoint,
@@ -113,8 +119,8 @@ func NewManager(mountpoint string, group string, resources *Resources) (*Manager
 }
 
 func LoadManager(mountpoint string, group string) (*Manager, error) {
-	if err := VerifyGroupPath(group); err != nil {
-		return nil, err
+	if group == "" {
+		return nil, ErrInvalidGroupPath
 	}
 	path := filepath.Join(mountpoint, group)
 	return &Manager{
@@ -128,6 +134,58 @@ type Manager struct {
 	path              string
 }
 
+func (c *Manager) ListControllers() ([]string, error) {
+	f, err := os.Open(filepath.Join(c.path, subtreeControl))
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	var (
+		out []string
+		s   = bufio.NewScanner(f)
+	)
+	s.Split(bufio.ScanWords)
+	for s.Scan() {
+		if err := s.Err(); err != nil {
+			return nil, err
+		}
+		out = append(out, s.Text())
+	}
+	return out, nil
+}
+
+type ControllerToggle int
+
+const (
+	Enable ControllerToggle = iota + 1
+	Disable
+)
+
+func toggleFunc(controllers []string, prefix string) []string {
+	out := make([]string, len(controllers))
+	for i, c := range controllers {
+		out[i] = prefix + c
+	}
+	return out
+}
+
+func (c *Manager) ToggleControllers(controllers []string, t ControllerToggle) error {
+	f, err := os.OpenFile(filepath.Join(c.path, subtreeControl), os.O_WRONLY, 0)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	switch t {
+	case Enable:
+		controllers = toggleFunc(controllers, "+")
+	case Disable:
+		controllers = toggleFunc(controllers, "-")
+	}
+	_, err = f.WriteString(strings.Join(controllers, " "))
+	return err
+}
+
 func (c *Manager) NewChild(name string, resources *Resources) (*Manager, error) {
 	if strings.HasPrefix(name, "/") {
 		return nil, errors.New("name must be relative")
diff --git a/v2/paths.go b/v2/paths.go
index 75d450f2..171e45bd 100644
--- a/v2/paths.go
+++ b/v2/paths.go
@@ -19,7 +19,6 @@ package v2
 import (
 	"fmt"
 	"path/filepath"
-	"strings"
 )
 
 // NestedGroupPath will nest the cgroups based on the calling processes cgroup
@@ -38,16 +37,3 @@ func PidGroupPath(pid int) (string, error) {
 	p := fmt.Sprintf("/proc/%d/cgroup", pid)
 	return parseCgroupFile(p)
 }
-
-// VerifyGroupPath verifies the format of g.
-// VerifyGroupPath doesn't verify whether g actually exists on the system.
-func VerifyGroupPath(g string) error {
-	s := string(g)
-	if !strings.HasPrefix(s, "/") {
-		return ErrInvalidGroupPath
-	}
-	if strings.HasPrefix(s, "/sys/fs/cgroup") {
-		return ErrInvalidGroupPath
-	}
-	return nil
-}