diff --git a/cmd/climc/shell/compute/containers.go b/cmd/climc/shell/compute/containers.go index de62fda694..b492f732d9 100644 --- a/cmd/climc/shell/compute/containers.go +++ b/cmd/climc/shell/compute/containers.go @@ -19,6 +19,7 @@ import ( "io/ioutil" "os" "os/exec" + "strings" "github.com/ghodss/yaml" @@ -30,6 +31,7 @@ import ( "yunion.io/x/onecloud/pkg/mcclient" modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" options "yunion.io/x/onecloud/pkg/mcclient/options/compute" + "yunion.io/x/onecloud/pkg/util/pod/stream/cp" ) func init() { @@ -133,4 +135,53 @@ func init() { } return nil }) + + R(new(options.ContainerCopyOptions), "container-cp", "Container copy", func(s *mcclient.ClientSession, opts *options.ContainerCopyOptions) error { + parts := strings.Split(opts.CONTAINER_ID_FILE, ":") + if len(parts) != 2 { + return fmt.Errorf("invalid container id: %s", opts.CONTAINER_ID_FILE) + } + if opts.RawFile { + fr, err := os.Open(opts.SRC_FILE) + if err != nil { + return errors.Wrapf(err, "open file: %v", opts.SRC_FILE) + } + defer fr.Close() + if err := modules.Containers.CopyTo(s, parts[0], parts[1], fr); err != nil { + return errors.Wrapf(err, "copy file to container") + } + return nil + } else { + return cp.NewCopy().CopyToContainer(s, opts.SRC_FILE, cp.ContainerFileOpt{ + ContainerId: parts[0], + File: parts[1], + }) + } + }) + + R(new(options.ContainerCopyOptions), "container-cp-from", "Container copy", func(s *mcclient.ClientSession, opts *options.ContainerCopyOptions) error { + parts := strings.Split(opts.SRC_FILE, ":") + if len(parts) != 2 { + return fmt.Errorf("invalid container id: %s", opts.CONTAINER_ID_FILE) + } + ctrId := parts[0] + ctrFile := parts[1] + destFile := opts.CONTAINER_ID_FILE + if opts.RawFile { + fw, err := os.Create(destFile) + if err != nil { + return errors.Wrapf(err, "open file: %v", destFile) + } + defer fw.Close() + if err := modules.Containers.CopyFrom(s, ctrId, ctrFile, fw); err != nil { + return errors.Wrap(err, "copy from") + } + return nil + } else { + return cp.NewCopy().CopyFromContainer(s, cp.ContainerFileOpt{ + ContainerId: ctrId, + File: ctrFile, + }, destFile) + } + }) } diff --git a/pkg/apis/compute/container.go b/pkg/apis/compute/container.go index e2aa5dd4dc..8aec8d38dd 100644 --- a/pkg/apis/compute/container.go +++ b/pkg/apis/compute/container.go @@ -192,6 +192,9 @@ type ContainerExecInfoOutput struct { type ContainerExecInput struct { Command []string `json:"command"` Tty bool `json:"tty"` + SetIO bool `json:"set_io"` + Stdin bool `json:"stdin"` + Stdout bool `json:"stdout"` } type ContainerExecSyncInput struct { diff --git a/pkg/hostman/guestman/pod.go b/pkg/hostman/guestman/pod.go index bfd63837c9..9492e435fa 100644 --- a/pkg/hostman/guestman/pod.go +++ b/pkg/hostman/guestman/pod.go @@ -2085,13 +2085,21 @@ func (s *sPodGuestInstance) ExecContainer(ctx context.Context, userCred mcclient if err != nil { return nil, errors.Wrap(err, "get container cri id") } + stderr := true + if input.Tty { + stderr = false + } req := &runtimeapi.ExecRequest{ ContainerId: criId, Cmd: input.Command, Tty: input.Tty, Stdin: true, Stdout: true, - //Stderr: true, + Stderr: stderr, + } + if input.SetIO { + req.Stdin = input.Stdin + req.Stdout = input.Stdout } resp, err := rCli.Exec(ctx, req) if err != nil { diff --git a/pkg/mcclient/modules/compute/mod_containers.go b/pkg/mcclient/modules/compute/mod_containers.go index 0a2a13701d..c9553b8095 100644 --- a/pkg/mcclient/modules/compute/mod_containers.go +++ b/pkg/mcclient/modules/compute/mod_containers.go @@ -21,9 +21,11 @@ import ( "io" "net/url" "os" + "path" "time" "yunion.io/x/jsonutils" + "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/httputils" @@ -36,18 +38,32 @@ import ( "yunion.io/x/onecloud/pkg/util/pod/term" ) +var ( + Containers ContainerManager +) + +func init() { + Containers = ContainerManager{ + modules.NewComputeManager("container", "containers", + []string{"ID", "Name", "Guest_ID", "Status", "Started_At", "Last_Finished_At", "Restart_Count", "Spec"}, + []string{}), + } + modules.RegisterCompute(&Containers) +} + type ContainerManager struct { modulebase.ResourceManager } func (man ContainerManager) SetupTTY(in io.Reader, out io.Writer, errOut io.Writer, raw bool) term.TTY { - /*t := term.TTY{ + t := term.TTY{ Out: out, } if in == nil { t.In = nil + t.Raw = false return t - }*/ + } return term.TTY{ In: in, Out: out, @@ -97,6 +113,54 @@ func (man ContainerManager) Exec(s *mcclient.ClientSession, id string, opt *api. return t.Safe(fn) } +type ContainerExecInput struct { + Command []string + Tty bool + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} + +func (man ContainerManager) ExecV2(s *mcclient.ClientSession, id string, opt *ContainerExecInput) error { + info, err := man.GetSpecific(s, id, "exec-info", nil) + if err != nil { + return errors.Wrap(err, "get exec info") + } + infoOut := new(api.ContainerExecInfoOutput) + info.Unmarshal(infoOut) + apiInput := &api.ContainerExecInput{ + Command: opt.Command, + Tty: opt.Tty, + SetIO: true, + Stdin: opt.Stdin != nil, + Stdout: opt.Stdout != nil, + } + urlLoc := fmt.Sprintf("%s/pods/%s/containers/%s/exec?%s", infoOut.HostUri, infoOut.PodId, infoOut.ContainerId, jsonutils.Marshal(apiInput).QueryString()) + url, err := url.Parse(urlLoc) + if err != nil { + return errors.Wrapf(err, "parse url: %s", urlLoc) + } + exec, err := remotecommand.NewSPDYExecutor("POST", url) + if err != nil { + return errors.Wrap(err, "NewSPDYExecutor") + } + headers := mcclient.GetTokenHeaders(s.GetToken()) + + t := man.SetupTTY(opt.Stdin, opt.Stdout, opt.Stderr, true) + sizeQueue := t.MonitorSize(t.GetSize()) + fn := func() error { + return exec.Stream(remotecommand.StreamOptions{ + Stdin: opt.Stdin, + Stdout: opt.Stdout, + Stderr: opt.Stderr, + Tty: opt.Tty, + TerminalSizeQueue: sizeQueue, + Header: headers, + }) + } + return t.Safe(fn) +} + func (man ContainerManager) Log(s *mcclient.ClientSession, id string, opt *api.PodLogOptions) (io.ReadCloser, error) { info, err := man.GetSpecific(s, id, "exec-info", nil) if err != nil { @@ -142,15 +206,61 @@ func (man ContainerManager) LogToWriter(s *mcclient.ClientSession, id string, op return nil } -var ( - Containers ContainerManager -) - -func init() { - Containers = ContainerManager{ - modules.NewComputeManager("container", "containers", - []string{"ID", "Name", "Guest_ID", "Status", "Started_At", "Last_Finished_At", "Restart_Count", "Spec"}, - []string{}), +func (man ContainerManager) EnsureDir(s *mcclient.ClientSession, ctrId string, dirName string) error { + opt := &ContainerExecInput{ + Command: []string{"mkdir", "-p", dirName}, + Tty: false, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, } - modules.RegisterCompute(&Containers) + return man.ExecV2(s, ctrId, opt) +} + +func (man ContainerManager) CopyTo(s *mcclient.ClientSession, ctrId string, destPath string, in io.Reader) error { + destDir := path.Dir(destPath) + if err := man.EnsureDir(s, ctrId, destDir); err != nil { + return errors.Wrapf(err, "ensure dir %s", destDir) + } + + reader, writer := io.Pipe() + go func() { + defer writer.Close() + written, err := io.Copy(writer, in) + if err != nil { + log.Errorf("copy reader to writer, written %d, error: %v", written, err) + } + }() + + ctrCmd := []string{"sh", "-c", fmt.Sprintf("cat - > %s", destPath)} + opt := &ContainerExecInput{ + Command: ctrCmd, + Tty: false, + Stdin: reader, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + return man.ExecV2(s, ctrId, opt) +} + +func (man ContainerManager) CopyFrom(s *mcclient.ClientSession, ctrId string, ctrFile string, out io.Writer) error { + reader, outStream := io.Pipe() + opts := &ContainerExecInput{ + Command: []string{"cat", ctrFile}, + Tty: false, + Stdin: nil, + Stdout: outStream, + Stderr: os.Stderr, + } + go func() { + defer outStream.Close() + if err := man.ExecV2(s, ctrId, opts); err != nil { + log.Errorf("compute.Containers.ExecV2: %v", err) + } + }() + written, err := io.Copy(out, reader) + if err != nil { + return errors.Wrapf(err, "copy from reader written: %d", written) + } + return nil } diff --git a/pkg/mcclient/options/compute/containers.go b/pkg/mcclient/options/compute/containers.go index e509ebdffa..47fa2aa490 100644 --- a/pkg/mcclient/options/compute/containers.go +++ b/pkg/mcclient/options/compute/containers.go @@ -497,3 +497,9 @@ func (o *ContainerRemoveVolumeMountPostOverlayOptions) Params() (jsonutils.JSONO } return params, nil } + +type ContainerCopyOptions struct { + SRC_FILE string + CONTAINER_ID_FILE string + RawFile bool +} diff --git a/pkg/util/pod/stream/cp/cp.go b/pkg/util/pod/stream/cp/cp.go new file mode 100644 index 0000000000..f2bb67bc5f --- /dev/null +++ b/pkg/util/pod/stream/cp/cp.go @@ -0,0 +1,321 @@ +// Copyright 2019 Yunion +// +// 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 cp + +import ( + "archive/tar" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules/compute" +) + +type ICopy interface { + CopyToContainer(s *mcclient.ClientSession, srcPath string, dest ContainerFileOpt) error + CopyFromContainer(s *mcclient.ClientSession, src ContainerFileOpt, destPath string) error +} + +type sCopy struct { + noPreserve bool + ExecParentCmdName string +} + +func NewCopy() ICopy { + return &sCopy{} +} + +type ContainerFileOpt struct { + ContainerId string + File string +} + +var ErrFileCannotBeEmpty = errors.Error("filepath can not be empty") + +func (o *sCopy) CopyFromContainer(s *mcclient.ClientSession, src ContainerFileOpt, destPath string) error { + if len(src.File) == 0 || len(destPath) == 0 { + return ErrFileCannotBeEmpty + } + + reader, outStream := io.Pipe() + opts := &compute.ContainerExecInput{ + Command: []string{"tar", "cf", "-", src.File}, + Tty: false, + Stdin: nil, + Stdout: outStream, + Stderr: os.Stderr, + } + + go func() { + defer outStream.Close() + if err := compute.Containers.ExecV2(s, src.ContainerId, opts); err != nil { + log.Errorf("compute.Containers.ExecV2: %v", err) + } + }() + prefix := getPrefix(src.File) + prefix = path.Clean(prefix) + // remove extraneous path shortcuts - these could occur if a path contained extra "../ + // and attempted to navigate beyond "/" in a remote filesystem + prefix = stripPathShortcuts(prefix) + return o.untarAll(src, reader, destPath, prefix) +} + +func getPrefix(file string) string { + // tar strips the leading '/' if it's there, so we will too + return strings.TrimLeft(file, "/") +} + +// stripPathShortcuts removes any leading or trailing "../" from a given path +func stripPathShortcuts(p string) string { + newPath := path.Clean(p) + trimmed := strings.TrimPrefix(newPath, "../") + + for trimmed != newPath { + newPath = trimmed + trimmed = strings.TrimPrefix(newPath, "../") + } + + // trim leftover {".", ".."} + if newPath == "." || newPath == ".." { + newPath = "" + } + + if len(newPath) > 0 && string(newPath[0]) == "/" { + return newPath[1:] + } + + return newPath +} + +func (o *sCopy) untarAll(src ContainerFileOpt, reader io.Reader, destDir, prefix string) error { + symlinkWarningPrinted := false + // TODO: use compression here? + tarReader := tar.NewReader(reader) + for { + header, err := tarReader.Next() + if err != nil { + if err != io.EOF { + return err + } + break + } + + // All the files will start with the prefix, which is the directory where + // they were located on the pod, we need to strip down that prefix, but + // if the prefix is missing it means the tar was tempered with. + // For the case where prefix is empty we need to ensure that the path + // is not absolute, which also indicates the tar file was tempered with. + if !strings.HasPrefix(header.Name, prefix) { + return fmt.Errorf("tar contents corrupted") + } + + // basic file information + mode := header.FileInfo().Mode() + destFileName := filepath.Join(destDir, header.Name[len(prefix):]) + + if !isDestRelative(destDir, destFileName) { + fmt.Fprintf(os.Stderr, "warning: file %q is outside target destination, skipping\n", destFileName) + continue + } + + baseName := filepath.Dir(destFileName) + if err := os.MkdirAll(baseName, 0755); err != nil { + return err + } + if header.FileInfo().IsDir() { + if err := os.MkdirAll(destFileName, 0755); err != nil { + return err + } + continue + } + + if mode&os.ModeSymlink != 0 { + if !symlinkWarningPrinted && len(o.ExecParentCmdName) > 0 { + fmt.Fprintf(os.Stderr, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname) + symlinkWarningPrinted = true + continue + } + fmt.Fprintf(os.Stderr, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname) + continue + } + outFile, err := os.Create(destFileName) + if err != nil { + return err + } + defer outFile.Close() + if _, err := io.Copy(outFile, tarReader); err != nil { + return err + } + if err := outFile.Close(); err != nil { + return err + } + } + + return nil +} + +// isDestRelative returns true if dest is pointing outside the base directory, +// false otherwise. +func isDestRelative(base, dest string) bool { + relative, err := filepath.Rel(base, dest) + if err != nil { + return false + } + return relative == "." || relative == stripPathShortcuts(relative) +} + +func (o *sCopy) CopyToContainer(s *mcclient.ClientSession, srcFile string, dest ContainerFileOpt) error { + if len(srcFile) == 0 || len(dest.File) == 0 { + return ErrFileCannotBeEmpty + } + if _, err := os.Stat(srcFile); err != nil { + return errors.Wrapf(err, "check source file: %s", srcFile) + } + reader, writer := io.Pipe() + // strip trailing slash (if any) + if dest.File != "/" && strings.HasSuffix(string(dest.File[len(dest.File)-1]), "/") { + dest.File = dest.File[:len(dest.File)-1] + } + if err := o.checkDestinationIsDir(s, dest); err == nil { + // If no error, dest.File was found to be a directory. + // Copy specified src info it + dest.File = dest.File + "/" + path.Base(srcFile) + } + + go func() { + defer writer.Close() + if err := makeTar(srcFile, dest.File, writer); err != nil { + log.Errorf("makeTar error: %v", err) + } + }() + var cmdArr []string + + if o.noPreserve { + cmdArr = []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-"} + } else { + cmdArr = []string{"tar", "-xmf", "-"} + } + destDir := path.Dir(dest.File) + if len(destDir) > 0 { + cmdArr = append(cmdArr, "-C", destDir) + } + + opt := &compute.ContainerExecInput{ + Command: cmdArr, + Tty: false, + Stdin: reader, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + return compute.Containers.ExecV2(s, dest.ContainerId, opt) +} + +func (o *sCopy) checkDestinationIsDir(s *mcclient.ClientSession, dest ContainerFileOpt) error { + opt := &compute.ContainerExecInput{ + Command: []string{"test", "-d", dest.File}, + Tty: false, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + return compute.Containers.ExecV2(s, dest.ContainerId, opt) +} + +func makeTar(srcPath, destPath string, writer io.Writer) error { + tarWriter := tar.NewWriter(writer) + defer tarWriter.Close() + + srcPath = path.Clean(srcPath) + destPath = path.Clean(destPath) + return recursiveTar(path.Dir(srcPath), path.Base(srcPath), path.Dir(destPath), path.Base(destPath), tarWriter) +} + +func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer) error { + srcPath := path.Join(srcBase, srcFile) + matchedPaths, err := filepath.Glob(srcPath) + if err != nil { + return err + } + for _, fpath := range matchedPaths { + stat, err := os.Lstat(fpath) + if err != nil { + return err + } + if stat.IsDir() { + files, err := ioutil.ReadDir(fpath) + if err != nil { + return err + } + if len(files) == 0 { + //case empty directory + hdr, _ := tar.FileInfoHeader(stat, fpath) + hdr.Name = destFile + if err := tw.WriteHeader(hdr); err != nil { + return err + } + } + for _, f := range files { + if err := recursiveTar(srcBase, path.Join(srcFile, f.Name()), destBase, path.Join(destFile, f.Name()), tw); err != nil { + return err + } + } + return nil + } else if stat.Mode()&os.ModeSymlink != 0 { + //case soft link + hdr, _ := tar.FileInfoHeader(stat, fpath) + target, err := os.Readlink(fpath) + if err != nil { + return err + } + + hdr.Linkname = target + hdr.Name = destFile + if err := tw.WriteHeader(hdr); err != nil { + return err + } + } else { + //case regular file or other file type like pipe + hdr, err := tar.FileInfoHeader(stat, fpath) + if err != nil { + return err + } + hdr.Name = destFile + + if err := tw.WriteHeader(hdr); err != nil { + return err + } + + f, err := os.Open(fpath) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(tw, f); err != nil { + return err + } + return f.Close() + } + } + return nil +} diff --git a/pkg/util/pod/stream/cp/doc.go b/pkg/util/pod/stream/cp/doc.go new file mode 100644 index 0000000000..c9c5f608ba --- /dev/null +++ b/pkg/util/pod/stream/cp/doc.go @@ -0,0 +1 @@ +package cp // import "yunion.io/x/onecloud/pkg/util/pod/stream/cp"