feat(climc,host): support container copy and copy-from (#21822)

This commit is contained in:
Zexi Li
2024-12-18 16:55:33 +08:00
committed by GitHub
parent 4132f245ac
commit c2bc6b1c99
7 changed files with 513 additions and 13 deletions
+51
View File
@@ -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)
}
})
}
+3
View File
@@ -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 {
+9 -1
View File
@@ -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 {
+122 -12
View File
@@ -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
}
@@ -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
}
+321
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
package cp // import "yunion.io/x/onecloud/pkg/util/pod/stream/cp"