464 lines
15 KiB
Go
464 lines
15 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors All rights reserved.
|
|
|
|
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 oci
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"bufio"
|
|
"bytes"
|
|
|
|
"github.com/golang/glog"
|
|
"github.com/pkg/errors"
|
|
"k8s.io/minikube/pkg/minikube/constants"
|
|
"k8s.io/minikube/pkg/minikube/localpath"
|
|
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// DeleteAllContainersByLabel deletes all containers that have a specific label
|
|
// if there no containers found with the given label, it will return nil
|
|
func DeleteAllContainersByLabel(ociBin string, label string) []error {
|
|
var deleteErrs []error
|
|
if ociBin == Docker {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return []error{errors.Wrap(err, "point host docker-daemon")}
|
|
}
|
|
}
|
|
cs, err := listContainersByLabel(ociBin, label)
|
|
if err != nil {
|
|
return []error{fmt.Errorf("listing containers by label %q", label)}
|
|
}
|
|
if len(cs) == 0 {
|
|
return nil
|
|
}
|
|
for _, c := range cs {
|
|
cmd := exec.Command(ociBin, "rm", "-f", "-v", c)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
deleteErrs = append(deleteErrs, errors.Wrapf(err, "delete container %s: output %s", c, out))
|
|
}
|
|
}
|
|
return deleteErrs
|
|
}
|
|
|
|
// CreateContainerNode creates a new container node
|
|
func CreateContainerNode(p CreateParams) error {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
|
|
runArgs := []string{
|
|
"-d", // run the container detached
|
|
"-t", // allocate a tty for entrypoint logs
|
|
// running containers in a container requires privileged
|
|
// NOTE: we could try to replicate this with --cap-add, and use less
|
|
// privileges, but this flag also changes some mounts that are necessary
|
|
// including some ones docker would otherwise do by default.
|
|
// for now this is what we want. in the future we may revisit this.
|
|
"--privileged",
|
|
"--security-opt", "seccomp=unconfined", // also ignore seccomp
|
|
"--tmpfs", "/tmp", // various things depend on working /tmp
|
|
"--tmpfs", "/run", // systemd wants a writable /run
|
|
// logs,pods be stroed on filesystem vs inside container,
|
|
// some k8s things want /lib/modules
|
|
"-v", "/lib/modules:/lib/modules:ro",
|
|
"--hostname", p.Name, // make hostname match container name
|
|
"--name", p.Name, // ... and set the container name
|
|
"--label", fmt.Sprintf("%s=%s", CreatedByLabelKey, "true"),
|
|
// label the node with the cluster ID
|
|
"--label", p.ClusterLabel,
|
|
// label the node with the role ID
|
|
"--label", fmt.Sprintf("%s=%s", nodeRoleLabelKey, p.Role),
|
|
}
|
|
|
|
if p.OCIBinary == Podman { // enable execing in /var
|
|
// volume path in minikube home folder to mount to /var
|
|
hostVarVolPath := filepath.Join(localpath.MiniPath(), "machines", p.Name, "var")
|
|
if err := os.MkdirAll(hostVarVolPath, 0711); err != nil {
|
|
return errors.Wrapf(err, "create var dir %s", hostVarVolPath)
|
|
}
|
|
// podman mounts var/lib with no-exec by default https://github.com/containers/libpod/issues/5103
|
|
runArgs = append(runArgs, "--volume", fmt.Sprintf("%s:/var:exec", hostVarVolPath))
|
|
}
|
|
if p.OCIBinary == Docker {
|
|
if err := createDockerVolume(p.Name); err != nil {
|
|
return errors.Wrapf(err, "creating volume for %s container", p.Name)
|
|
}
|
|
glog.Infof("Successfully created a docker volume %s", p.Name)
|
|
runArgs = append(runArgs, "--volume", fmt.Sprintf("%s:%s", p.Name, "/var"))
|
|
// setting resource limit in privileged mode is only supported by docker
|
|
// podman error: "Error: invalid configuration, cannot set resources with rootless containers not using cgroups v2 unified mode"
|
|
runArgs = append(runArgs, fmt.Sprintf("--cpus=%s", p.CPUs), fmt.Sprintf("--memory=%s", p.Memory))
|
|
}
|
|
|
|
for key, val := range p.Envs {
|
|
runArgs = append(runArgs, "-e", fmt.Sprintf("%s=%s", key, val))
|
|
}
|
|
|
|
// adds node specific args
|
|
runArgs = append(runArgs, p.ExtraArgs...)
|
|
|
|
enabled, err := isUsernsRemapEnabled(p.OCIBinary)
|
|
if err != nil {
|
|
glog.Warningf("Failed to detect if userns is enabled: %v", err)
|
|
}
|
|
if enabled {
|
|
// We need this argument in order to make this command work
|
|
// in systems that have userns-remap enabled on the docker daemon
|
|
runArgs = append(runArgs, "--userns=host")
|
|
}
|
|
|
|
_, err = createContainer(p.OCIBinary,
|
|
p.Image,
|
|
withRunArgs(runArgs...),
|
|
withMounts(p.Mounts),
|
|
withPortMappings(p.PortMappings),
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "create a kic node")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateContainer creates a container with "docker/podman run"
|
|
func createContainer(ociBinary string, image string, opts ...createOpt) ([]string, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return nil, errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
|
|
o := &createOpts{}
|
|
for _, opt := range opts {
|
|
o = opt(o)
|
|
}
|
|
// convert mounts to container run args
|
|
runArgs := o.RunArgs
|
|
for _, mount := range o.Mounts {
|
|
runArgs = append(runArgs, generateMountBindings(mount)...)
|
|
}
|
|
for _, portMapping := range o.PortMappings {
|
|
runArgs = append(runArgs, generatePortMappings(portMapping)...)
|
|
}
|
|
// construct the actual docker run argv
|
|
args := []string{"run"}
|
|
// to run nested container from privileged container in podman https://bugzilla.redhat.com/show_bug.cgi?id=1687713
|
|
if ociBinary == Podman {
|
|
args = append(args, "--cgroup-manager", "cgroupfs")
|
|
}
|
|
args = append(args, runArgs...)
|
|
args = append(args, image)
|
|
args = append(args, o.ContainerArgs...)
|
|
cmd := exec.Command(ociBinary, args...)
|
|
var buff bytes.Buffer
|
|
cmd.Stdout = &buff
|
|
cmd.Stderr = &buff
|
|
err := cmd.Run()
|
|
scanner := bufio.NewScanner(&buff)
|
|
var output []string
|
|
for scanner.Scan() {
|
|
output = append(output, scanner.Text())
|
|
}
|
|
|
|
if err != nil {
|
|
return output, errors.Wrapf(err, "args: %v output: %s ", args, output)
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
// Copy copies a local asset into the container
|
|
func Copy(ociBinary string, ociID string, targetDir string, fName string) error {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
if _, err := os.Stat(fName); os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "error source %s does not exist", fName)
|
|
}
|
|
destination := fmt.Sprintf("%s:%s", ociID, targetDir)
|
|
cmd := exec.Command(ociBinary, "cp", fName, destination)
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error copying %s into node", fName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HostPortBinding will return port mapping for a container using cli.
|
|
// example : HostPortBinding("docker", "minikube", "22")
|
|
// will return the docker assigned port:
|
|
// 32769, nil
|
|
// only supports TCP ports
|
|
func HostPortBinding(ociBinary string, ociID string, contPort int) (int, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return 0, errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
var out []byte
|
|
var err error
|
|
if ociBinary == Podman {
|
|
//podman inspect -f "{{range .NetworkSettings.Ports}}{{if eq .ContainerPort "80"}}{{.HostPort}}{{end}}{{end}}"
|
|
cmd := exec.Command(ociBinary, "inspect", "-f", fmt.Sprintf("{{range .NetworkSettings.Ports}}{{if eq .ContainerPort %s}}{{.HostPort}}{{end}}{{end}}", fmt.Sprint(contPort)), ociID)
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "get host-bind port %d for %q, output %s", contPort, ociID, out)
|
|
}
|
|
} else {
|
|
cmd := exec.Command(ociBinary, "inspect", "-f", fmt.Sprintf("'{{(index (index .NetworkSettings.Ports \"%d/tcp\") 0).HostPort}}'", contPort), ociID)
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "get host-bind port %d for %q, output %s", contPort, ociID, out)
|
|
}
|
|
}
|
|
|
|
o := strings.TrimSpace(string(out))
|
|
o = strings.Trim(o, "'")
|
|
p, err := strconv.Atoi(o)
|
|
if err != nil {
|
|
return p, errors.Wrapf(err, "convert host-port %q to number", p)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// ContainerIPs returns ipv4,ipv6, error of a container by their name
|
|
func ContainerIPs(ociBinary string, name string) (string, string, error) {
|
|
if ociBinary == Podman {
|
|
return podmanConttainerIP(name)
|
|
}
|
|
return dockerContainerIP(name)
|
|
}
|
|
|
|
// podmanConttainerIP returns ipv4, ipv6 of container or error
|
|
func podmanConttainerIP(name string) (string, string, error) {
|
|
cmd := exec.Command(Podman, "inspect",
|
|
"-f", "{{.NetworkSettings.IPAddress}}",
|
|
name)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", "", errors.Wrapf(err, "podman inspect ip %s", name)
|
|
}
|
|
output := strings.TrimSpace(string(out))
|
|
if err == nil && output == "" { // podman returns empty for 127.0.0.1
|
|
return DefaultBindIPV4, "", nil
|
|
}
|
|
return output, "", nil
|
|
}
|
|
|
|
// dockerContainerIP returns ipv4, ipv6 of container or error
|
|
func dockerContainerIP(name string) (string, string, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return "", "", errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
// retrieve the IP address of the node using docker inspect
|
|
lines, err := inspect(Docker, name, "{{range .NetworkSettings.Networks}}{{.IPAddress}},{{.GlobalIPv6Address}}{{end}}")
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "inspecting NetworkSettings.Networks")
|
|
}
|
|
if len(lines) != 1 {
|
|
return "", "", errors.Errorf("IPs output should only be one line, got %d lines", len(lines))
|
|
}
|
|
ips := strings.Split(lines[0], ",")
|
|
if len(ips) != 2 {
|
|
return "", "", errors.Errorf("container addresses should have 2 values, got %d values: %+v", len(ips), ips)
|
|
}
|
|
return ips[0], ips[1], nil
|
|
}
|
|
|
|
// ContainerID returns id of a container name
|
|
func ContainerID(ociBinary string, nameOrID string) (string, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return "", errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
cmd := exec.Command(ociBinary, "inspect", "-f", "{{.Id}}", nameOrID)
|
|
id, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
id = []byte{}
|
|
}
|
|
return string(id), err
|
|
}
|
|
|
|
// ListOwnedContainers lists all the containres that kic driver created on user's machine using a label
|
|
func ListOwnedContainers(ociBinary string) ([]string, error) {
|
|
return listContainersByLabel(ociBinary, ProfileLabelKey)
|
|
}
|
|
|
|
// inspect return low-level information on containers
|
|
func inspect(ociBinary string, containerNameOrID, format string) ([]string, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return nil, errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
cmd := exec.Command(ociBinary, "inspect",
|
|
"-f", format,
|
|
containerNameOrID) // ... against the "node" container
|
|
var buff bytes.Buffer
|
|
cmd.Stdout = &buff
|
|
cmd.Stderr = &buff
|
|
err := cmd.Run()
|
|
scanner := bufio.NewScanner(&buff)
|
|
var lines []string
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
return lines, err
|
|
}
|
|
|
|
/*
|
|
This is adapated from:
|
|
https://github.com/kubernetes/kubernetes/blob/07a5488b2a8f67add543da72e8819407d8314204/pkg/kubelet/dockershim/helpers.go#L115-L155
|
|
*/
|
|
// generateMountBindings converts the mount list to a list of strings that
|
|
// can be understood by docker
|
|
// '<HostPath>:<ContainerPath>[:options]', where 'options'
|
|
// is a comma-separated list of the following strings:
|
|
// 'ro', if the path is read only
|
|
// 'Z', if the volume requires SELinux relabeling
|
|
func generateMountBindings(mounts ...Mount) []string {
|
|
result := make([]string, 0, len(mounts))
|
|
for _, m := range mounts {
|
|
bind := fmt.Sprintf("%s:%s", m.HostPath, m.ContainerPath)
|
|
var attrs []string
|
|
if m.Readonly {
|
|
attrs = append(attrs, "ro")
|
|
}
|
|
// Only request relabeling if the pod provides an SELinux context. If the pod
|
|
// does not provide an SELinux context relabeling will label the volume with
|
|
// the container's randomly allocated MCS label. This would restrict access
|
|
// to the volume to the container which mounts it first.
|
|
if m.SelinuxRelabel {
|
|
attrs = append(attrs, "Z")
|
|
}
|
|
switch m.Propagation {
|
|
case MountPropagationNone:
|
|
// noop, private is default
|
|
case MountPropagationBidirectional:
|
|
attrs = append(attrs, "rshared")
|
|
case MountPropagationHostToContainer:
|
|
attrs = append(attrs, "rslave")
|
|
default:
|
|
// Falls back to "private"
|
|
}
|
|
|
|
if len(attrs) > 0 {
|
|
bind = fmt.Sprintf("%s:%s", bind, strings.Join(attrs, ","))
|
|
}
|
|
// our specific modification is the following line: make this a docker flag
|
|
bind = fmt.Sprintf("--volume=%s", bind)
|
|
result = append(result, bind)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// isUsernsRemapEnabled checks if userns-remap is enabled in docker
|
|
func isUsernsRemapEnabled(ociBinary string) (bool, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return false, errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
cmd := exec.Command(ociBinary, "info", "--format", "'{{json .SecurityOptions}}'")
|
|
var buff bytes.Buffer
|
|
cmd.Stdout = &buff
|
|
cmd.Stderr = &buff
|
|
err := cmd.Run()
|
|
scanner := bufio.NewScanner(&buff)
|
|
var lines []string
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
if len(lines) > 0 {
|
|
if strings.Contains(lines[0], "name=userns") {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func generatePortMappings(portMappings ...PortMapping) []string {
|
|
result := make([]string, 0, len(portMappings))
|
|
for _, pm := range portMappings {
|
|
// let docker pick a host port by leaving it as ::
|
|
// example --publish=127.0.0.17::8443 will get a random host port for 8443
|
|
publish := fmt.Sprintf("--publish=%s::%d", pm.ListenAddress, pm.ContainerPort)
|
|
result = append(result, publish)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// withRunArgs sets the args for docker run
|
|
// as in the args portion of `docker run args... image containerArgs...`
|
|
func withRunArgs(args ...string) createOpt {
|
|
return func(r *createOpts) *createOpts {
|
|
r.RunArgs = args
|
|
return r
|
|
}
|
|
}
|
|
|
|
// withMounts sets the container mounts
|
|
func withMounts(mounts []Mount) createOpt {
|
|
return func(r *createOpts) *createOpts {
|
|
r.Mounts = mounts
|
|
return r
|
|
}
|
|
}
|
|
|
|
// withPortMappings sets the container port mappings to the host
|
|
func withPortMappings(portMappings []PortMapping) createOpt {
|
|
return func(r *createOpts) *createOpts {
|
|
r.PortMappings = portMappings
|
|
return r
|
|
}
|
|
}
|
|
|
|
// listContainersByLabel returns all the container names with a specified label
|
|
func listContainersByLabel(ociBinary string, label string) ([]string, error) {
|
|
if err := PointToHostDockerDaemon(); err != nil {
|
|
return nil, errors.Wrap(err, "point host docker-daemon")
|
|
}
|
|
cmd := exec.Command(ociBinary, "ps", "-a", "--filter", fmt.Sprintf("label=%s", label), "--format", "{{.Names}}")
|
|
stdout, err := cmd.Output()
|
|
s := bufio.NewScanner(bytes.NewReader(stdout))
|
|
var names []string
|
|
for s.Scan() {
|
|
n := strings.TrimSpace(s.Text())
|
|
if n != "" {
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
|
|
return names, err
|
|
}
|
|
|
|
// PointToHostDockerDaemon will unset env variables that point to docker inside minikube
|
|
// to make sure it points to the docker daemon installed by user.
|
|
func PointToHostDockerDaemon() error {
|
|
p := os.Getenv(constants.MinikubeActiveDockerdEnv)
|
|
if p != "" {
|
|
glog.Infof("shell is pointing to docker inside minikube. will unset to use host")
|
|
}
|
|
|
|
for i := range constants.DockerDaemonEnvs {
|
|
e := constants.DockerDaemonEnvs[i]
|
|
err := os.Setenv(e, "")
|
|
if err != nil {
|
|
return errors.Wrapf(err, "resetting %s env", e)
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|