303 lines
7.9 KiB
Go
303 lines
7.9 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 command
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/term"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/minikube/pkg/drivers/kic/oci"
|
|
"k8s.io/minikube/pkg/minikube/assets"
|
|
"k8s.io/minikube/pkg/minikube/detect"
|
|
)
|
|
|
|
// kicRunner runs commands inside a container
|
|
// It implements the CommandRunner interface.
|
|
type kicRunner struct {
|
|
nameOrID string
|
|
ociBin string
|
|
}
|
|
|
|
// NewKICRunner returns a kicRunner implementor of runner which runs cmds inside a container
|
|
func NewKICRunner(containerNameOrID string, oci string) Runner {
|
|
return &kicRunner{
|
|
nameOrID: containerNameOrID,
|
|
ociBin: oci, // docker or podman
|
|
}
|
|
}
|
|
|
|
func (k *kicRunner) RunCmd(cmd *exec.Cmd) (*RunResult, error) {
|
|
args := []string{
|
|
"exec",
|
|
// run with privileges so we can remount etc..
|
|
"--privileged",
|
|
}
|
|
if cmd.Stdin != nil {
|
|
args = append(args,
|
|
"-i", // interactive so we can supply input
|
|
)
|
|
}
|
|
// if the command is hooked to another processes's output we want a tty
|
|
if isTerminal(cmd.Stderr) || isTerminal(cmd.Stdout) {
|
|
args = append(args,
|
|
"-t",
|
|
)
|
|
}
|
|
|
|
for _, env := range cmd.Env {
|
|
args = append(args, "-e", env)
|
|
}
|
|
// append container name to docker arguments. all subsequent args
|
|
// appended will be passed to the container instead of docker
|
|
args = append(
|
|
args,
|
|
k.nameOrID, // ... against the container
|
|
)
|
|
|
|
args = append(
|
|
args,
|
|
cmd.Args...,
|
|
)
|
|
oc := exec.Command(k.ociBin, args...)
|
|
oc.Stdin = cmd.Stdin
|
|
oc.Stdout = cmd.Stdout
|
|
oc.Stderr = cmd.Stderr
|
|
oc.Env = cmd.Env
|
|
|
|
rr := &RunResult{Args: cmd.Args}
|
|
klog.Infof("Run: %v", rr.Command())
|
|
|
|
var outb, errb io.Writer
|
|
if oc.Stdout == nil {
|
|
var so bytes.Buffer
|
|
outb = io.MultiWriter(&so, &rr.Stdout)
|
|
} else {
|
|
outb = io.MultiWriter(oc.Stdout, &rr.Stdout)
|
|
}
|
|
|
|
if oc.Stderr == nil {
|
|
var se bytes.Buffer
|
|
errb = io.MultiWriter(&se, &rr.Stderr)
|
|
} else {
|
|
errb = io.MultiWriter(oc.Stderr, &rr.Stderr)
|
|
}
|
|
|
|
oc.Stdout = outb
|
|
oc.Stderr = errb
|
|
|
|
oc = oci.PrefixCmd(oc)
|
|
klog.Infof("Args: %v", oc.Args)
|
|
|
|
start := time.Now()
|
|
|
|
err := oc.Run()
|
|
elapsed := time.Since(start)
|
|
if err == nil {
|
|
// Reduce log spam
|
|
if elapsed > (1 * time.Second) {
|
|
klog.Infof("Done: %v: (%s)", oc.Args, elapsed)
|
|
}
|
|
return rr, nil
|
|
}
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
rr.ExitCode = exitError.ExitCode()
|
|
}
|
|
return rr, fmt.Errorf("%s: %v\nstdout:\n%s\nstderr:\n%s", rr.Command(), err, rr.Stdout.String(), rr.Stderr.String())
|
|
|
|
}
|
|
|
|
func (k *kicRunner) StartCmd(cmd *exec.Cmd) (*StartedCmd, error) {
|
|
return nil, fmt.Errorf("kicRunner does not support StartCmd - you could be the first to add it")
|
|
}
|
|
|
|
func (k *kicRunner) WaitCmd(sc *StartedCmd) (*RunResult, error) {
|
|
return nil, fmt.Errorf("kicRunner does not support WaitCmd - you could be the first to add it")
|
|
}
|
|
|
|
// Copy copies a file and its permissions
|
|
func (k *kicRunner) Copy(f assets.CopyableFile) error {
|
|
dst := path.Join(path.Join(f.GetTargetDir(), f.GetTargetName()))
|
|
|
|
// For tiny files, it's cheaper to overwrite than check
|
|
if f.GetLength() > 4096 {
|
|
exists, err := fileExists(k, f, dst)
|
|
if err != nil {
|
|
klog.Infof("existence error for %s: %v", dst, err)
|
|
}
|
|
if exists {
|
|
klog.Infof("copy: skipping %s (exists)", dst)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
src := f.GetSourcePath()
|
|
if f.GetLength() == 0 {
|
|
klog.Warningf("0 byte asset: %+v", f)
|
|
}
|
|
|
|
perms, err := strconv.ParseInt(f.GetPermissions(), 8, 0)
|
|
if err != nil || perms > 07777 {
|
|
return errors.Wrapf(err, "error converting permissions %s to integer", f.GetPermissions())
|
|
}
|
|
|
|
if src != assets.MemorySource {
|
|
// Take the fast path
|
|
fi, err := os.Stat(src)
|
|
if err == nil {
|
|
if fi.Mode() == os.FileMode(perms) {
|
|
klog.Infof("%s (direct): %s --> %s (%d bytes)", k.ociBin, src, dst, f.GetLength())
|
|
return k.copy(src, dst)
|
|
}
|
|
|
|
// If >1MB, avoid local copy
|
|
if fi.Size() > (1024 * 1024) {
|
|
klog.Infof("%s (chmod): %s --> %s (%d bytes)", k.ociBin, src, dst, f.GetLength())
|
|
if err := k.copy(src, dst); err != nil {
|
|
return err
|
|
}
|
|
return k.chmod(dst, f.GetPermissions())
|
|
}
|
|
}
|
|
}
|
|
klog.Infof("%s (temp): %s --> %s (%d bytes)", k.ociBin, src, dst, f.GetLength())
|
|
|
|
tmpFolder, err := tempDirectory(detect.MinikubeInstalledViaSnap(), detect.DockerInstalledViaSnap())
|
|
if err != nil {
|
|
return errors.Wrap(err, "determining temp directory")
|
|
}
|
|
|
|
tf, err := os.CreateTemp(tmpFolder, "tmpf-memory-asset")
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating temporary file")
|
|
}
|
|
defer os.Remove(tf.Name())
|
|
|
|
if err := writeFile(tf.Name(), f, os.FileMode(perms)); err != nil {
|
|
return errors.Wrap(err, "write")
|
|
}
|
|
return k.copy(tf.Name(), dst)
|
|
}
|
|
|
|
// CopyFrom copies a file
|
|
func (k *kicRunner) CopyFrom(f assets.CopyableFile) error {
|
|
src := f.GetTargetPath()
|
|
dst := f.GetSourcePath()
|
|
|
|
klog.Infof("%s (direct): %s --> %s", k.ociBin, src, dst)
|
|
return k.copyFrom(src, dst)
|
|
}
|
|
|
|
// tempDirectory returns the directory to use as the temp directory
|
|
// or an empty string if it should use the os default temp directory.
|
|
func tempDirectory(isMinikubeSnap bool, isDockerSnap bool) (string, error) {
|
|
if !isMinikubeSnap && !isDockerSnap {
|
|
return "", nil
|
|
}
|
|
|
|
// Snap only allows an application to see its own files in /tmp, making Docker unable to copy memory assets
|
|
// https://github.com/kubernetes/minikube/issues/10020
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "detecting home dir")
|
|
}
|
|
return home, nil
|
|
}
|
|
|
|
func (k *kicRunner) copy(src string, dst string) error {
|
|
fullDest := fmt.Sprintf("%s:%s", k.nameOrID, dst)
|
|
if k.ociBin == oci.Podman {
|
|
return copyToPodman(src, fullDest)
|
|
}
|
|
return copyToDocker(src, fullDest)
|
|
}
|
|
|
|
func (k *kicRunner) copyFrom(src string, dst string) error {
|
|
fullSource := fmt.Sprintf("%s:%s", k.nameOrID, src)
|
|
if k.ociBin == oci.Podman {
|
|
return copyToPodman(fullSource, dst)
|
|
}
|
|
return copyToDocker(fullSource, dst)
|
|
}
|
|
|
|
func (k *kicRunner) chmod(dst string, perm string) error {
|
|
_, err := k.RunCmd(exec.Command("sudo", "chmod", perm, dst))
|
|
return err
|
|
}
|
|
|
|
// Podman cp command doesn't match docker and doesn't have -a
|
|
func copyToPodman(src string, dest string) error {
|
|
if runtime.GOOS == "linux" {
|
|
cmd := oci.PrefixCmd(exec.Command(oci.Podman, "cp", src, dest))
|
|
klog.Infof("Run: %v", cmd)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return errors.Wrapf(err, "podman copy %s into %s, output: %s", src, dest, string(out))
|
|
}
|
|
} else {
|
|
file, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
parts := strings.Split(dest, ":")
|
|
container := parts[0]
|
|
path := parts[1]
|
|
cmd := exec.Command(oci.Podman, "exec", "-i", container, "tee", path)
|
|
cmd.Stdin = file
|
|
klog.Infof("Run: %v", cmd)
|
|
if err := cmd.Run(); err != nil {
|
|
return errors.Wrapf(err, "podman copy %s into %s", src, dest)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyToDocker(src string, dest string) error {
|
|
if out, err := oci.PrefixCmd(exec.Command(oci.Docker, "cp", "-a", src, dest)).CombinedOutput(); err != nil {
|
|
return errors.Wrapf(err, "docker copy %s into %s, output: %s", src, dest, string(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove removes a file
|
|
func (k *kicRunner) Remove(f assets.CopyableFile) error {
|
|
dst := path.Join(f.GetTargetDir(), f.GetTargetName())
|
|
klog.Infof("rm: %s", dst)
|
|
|
|
_, err := k.RunCmd(exec.Command("sudo", "rm", dst))
|
|
return err
|
|
}
|
|
|
|
// isTerminal returns true if the writer w is a terminal
|
|
func isTerminal(w io.Writer) bool {
|
|
if v, ok := (w).(*os.File); ok {
|
|
return term.IsTerminal(int(v.Fd()))
|
|
}
|
|
return false
|
|
}
|