// +build linux

/*
Copyright 2016 The Kubernetes 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 system

import (
	"bufio"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

var _ Validator = &CgroupsValidator{}

// CgroupsValidator validates cgroup configuration.
type CgroupsValidator struct {
	Reporter Reporter
}

// Name is part of the system.Validator interface.
func (c *CgroupsValidator) Name() string {
	return "cgroups"
}

const (
	cgroupsConfigPrefix = "CGROUPS_"
	unifiedMountpoint   = "/sys/fs/cgroup"
)

// Validate is part of the system.Validator interface.
func (c *CgroupsValidator) Validate(spec SysSpec) (warns, errs []error) {
	// Get the subsystems from /sys/fs/cgroup/cgroup.controllers when cgroup v2 is used.
	// /proc/cgroups is meaningless for v2
	// https://github.com/torvalds/linux/blob/v5.3/Documentation/admin-guide/cgroup-v2.rst#deprecated-v1-core-features
	var st unix.Statfs_t
	var err error
	if err := unix.Statfs(unifiedMountpoint, &st); err != nil {
		return nil, []error{errors.Wrap(err, "cannot statfs the cgroupv2 root")}
	}
	var requiredCgroupSpec []string
	var optionalCgroupSpec []string
	var subsystems []string
	if st.Type == unix.CGROUP2_SUPER_MAGIC {
		subsystems, err = c.getCgroupV2Subsystems()
		if err != nil {
			return nil, []error{errors.Wrap(err, "failed to get cgroup v2 subsystems")}
		}
		requiredCgroupSpec = spec.CgroupsV2
		optionalCgroupSpec = spec.CgroupsV2Optional
	} else {
		subsystems, err = c.getCgroupV1Subsystems()
		if err != nil {
			return nil, []error{errors.Wrap(err, "failed to get cgroup v1 subsystems")}
		}
		requiredCgroupSpec = spec.Cgroups
		optionalCgroupSpec = spec.CgroupsOptional
	}

	if missingRequired := c.validateCgroupSubsystems(requiredCgroupSpec, subsystems, true); len(missingRequired) != 0 {
		errs = []error{errors.Errorf("missing required cgroups: %s", strings.Join(missingRequired, " "))}
	}
	if missingOptional := c.validateCgroupSubsystems(optionalCgroupSpec, subsystems, false); len(missingOptional) != 0 {
		warns = []error{errors.Errorf("missing optional cgroups: %s", strings.Join(missingOptional, " "))}
	}
	return
}

// validateCgroupSubsystems returns a list with the missing cgroups in the cgroup
func (c *CgroupsValidator) validateCgroupSubsystems(cgroups, subsystems []string, required bool) []string {
	var missing []string
	for _, cgroup := range cgroups {
		found := false
		for _, subsystem := range subsystems {
			if cgroup == subsystem {
				found = true
				break
			}
		}
		item := cgroupsConfigPrefix + strings.ToUpper(cgroup)
		if found {
			c.Reporter.Report(item, "enabled", good)
			continue
		} else if required {
			c.Reporter.Report(item, "missing", bad)
		} else {
			c.Reporter.Report(item, "missing", warn)
		}
		missing = append(missing, cgroup)
	}
	return missing

}

func (c *CgroupsValidator) getCgroupV1Subsystems() ([]string, error) {
	// Get the subsystems from /proc/cgroups when cgroup v1 is used.
	f, err := os.Open("/proc/cgroups")
	if err != nil {
		return nil, err
	}
	defer f.Close()

	subsystems := []string{}
	s := bufio.NewScanner(f)
	for s.Scan() {
		if err := s.Err(); err != nil {
			return nil, err
		}
		text := s.Text()
		if text[0] != '#' {
			parts := strings.Fields(text)
			if len(parts) >= 4 && parts[3] != "0" {
				subsystems = append(subsystems, parts[0])
			}
		}
	}
	return subsystems, nil
}

func (c *CgroupsValidator) getCgroupV2Subsystems() ([]string, error) {
	// Some controllers are implicitly enabled by the kernel.
	// Those controllers do not appear in /sys/fs/cgroup/cgroup.controllers.
	// https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L433-L434
	// We assume these are always available, as it is hard to detect availability.
	// So, we hardcode the following as "pseudo" controllers.
	// - devices: implemented in kernel 4.15
	// - freezer: implemented in kernel 5.2
	pseudo := []string{"devices", "freezer"}
	data, err := ioutil.ReadFile(filepath.Join(unifiedMountpoint, "cgroup.controllers"))
	if err != nil {
		return nil, err
	}
	subsystems := append(pseudo, strings.Fields(string(data))...)
	return subsystems, nil
}
