kubekey/pkg/modules/copy.go
William Wang 992a2259df
fix: failed to create local dir x permission denied (#2880)
* fix: failed to copy absolute file: failed to create local dir x permission denied

Signed-off-by: William Wang <williamw0825@gmail.com>

* docs: make log msg more accurate

Signed-off-by: William Wang <williamw0825@gmail.com>

* feat: ensure mode of dir when using src as dir in copy module

Signed-off-by: William Wang <williamw0825@gmail.com>

* refactor: change func name to ensureDestDirMode

Signed-off-by: William Wang <williamw0825@gmail.com>

* fix: pass go lint

Signed-off-by: William Wang <williamw0825@gmail.com>

---------

Signed-off-by: William Wang <williamw0825@gmail.com>
2025-12-09 17:39:18 +08:00

373 lines
11 KiB
Go

/*
Copyright 2023 The KubeSphere 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 modules
import (
"context"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"strings"
"github.com/cockroachdb/errors"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
"github.com/kubesphere/kubekey/v4/pkg/connector"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/project"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
/*
The Copy module copies files or content from local to remote hosts.
This module allows users to transfer files or create files with specified content on remote hosts.
Configuration:
Users can specify either a source file or content to copy.
copy:
src: /path/to/file # optional: local file path to copy
content: "text" # optional: content to write to file
dest: /remote/path # required: destination path on remote host
mode: 0644 # optional: file permissions (default: 0644)
Usage Examples in Playbook Tasks:
1. Copy local file:
```yaml
- name: Copy configuration file
copy:
src: config.yaml
dest: /etc/app/config.yaml
mode: 0644
register: copy_result
```
2. Create file with content:
```yaml
- name: Create config file
copy:
content: |
server: localhost
port: 8080
dest: /etc/app/config.yaml
register: config_result
```
3. Copy directory:
```yaml
- name: Copy application files
copy:
src: app/
dest: /opt/app/
register: app_files
```
Return Values:
- On success: Returns "Success" in stdout
- On failure: Returns error message in stderr
*/
// copyArgs holds the arguments for the copy module.
type copyArgs struct {
src string // Source file or directory path (local)
content string // Content to write to the destination file (if no src)
dest string // Destination path on the remote host
mode *uint32 // Optional file mode/permissions
}
// newCopyArgs parses and validates the arguments for the copy module.
func newCopyArgs(_ context.Context, raw runtime.RawExtension, vars map[string]any) (*copyArgs, error) {
var err error
ca := &copyArgs{}
args := variable.Extension2Variables(raw)
ca.src, _ = variable.StringVar(vars, args, "src")
ca.content, _ = variable.StringVar(vars, args, "content")
ca.dest, err = variable.StringVar(vars, args, "dest")
if err != nil {
return nil, errors.New("\"dest\" in args should be string")
}
mode, err := variable.IntVar(vars, args, "mode")
if err != nil {
klog.V(4).InfoS("get mode error", "error", err)
} else {
if *mode < 0 || *mode > math.MaxUint32 {
return nil, errors.New("mode should be uint32")
}
ca.mode = ptr.To(uint32(*mode))
}
return ca, nil
}
// ModuleCopy handles the "copy" module, copying files or content to remote hosts.
func ModuleCopy(ctx context.Context, options ExecOptions) (string, string, error) {
// get host variable
ha, err := options.getAllVariables()
if err != nil {
return StdoutFailed, StderrGetHostVariable, err
}
ca, err := newCopyArgs(ctx, options.Args, ha)
if err != nil {
return StdoutFailed, StderrParseArgument, err
}
// get connector
conn, err := options.getConnector(ctx)
if err != nil {
return StdoutFailed, StderrGetConnector, err
}
defer conn.Close(ctx)
switch {
case ca.src != "": // copy local file to remote
return ca.copySrc(ctx, options, conn)
case ca.content != "":
return ca.copyContent(ctx, os.ModePerm, conn)
default:
return StdoutFailed, StderrUnsupportArgs, errors.New("either \"src\" or \"content\" must be provided")
}
}
// copySrc copies the source file or directory to the destination on the remote host.
func (ca copyArgs) copySrc(ctx context.Context, options ExecOptions, conn connector.Connector) (string, string, error) {
if filepath.IsAbs(ca.src) { // if src is absolute path, find it in local path
return ca.handleAbsolutePath(ctx, conn)
}
// if src is not absolute path, find file in project
return ca.handleRelativePath(ctx, options, conn)
}
// handleAbsolutePath handles copying when the source is an absolute path.
func (ca copyArgs) handleAbsolutePath(ctx context.Context, conn connector.Connector) (string, string, error) {
fileInfo, err := os.Stat(ca.src)
if err != nil {
return StdoutFailed, "failed to stat absolute path", err
}
if fileInfo.IsDir() { // src is dir
if err := ca.copyAbsoluteDir(ctx, conn); err != nil {
return StdoutFailed, "failed to copy absolute dir", err
}
return StdoutSuccess, "", nil
}
// src is file
data, err := os.ReadFile(ca.src)
if err != nil {
return StdoutFailed, "failed to read absolute file", err
}
if err := ca.copyFile(ctx, data, fileInfo.Mode(), conn); err != nil {
return StdoutFailed, "failed to copy absolute file", err
}
return StdoutSuccess, "", nil
}
// handleRelativePath handles copying when the source is a relative path (from the project).
func (ca copyArgs) handleRelativePath(ctx context.Context, options ExecOptions, conn connector.Connector) (string, string, error) {
pj, err := project.New(ctx, options.Playbook, false)
if err != nil {
return StdoutFailed, StderrGetPlaybook, err
}
relPath := filepath.Join(options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath], _const.ProjectRolesFilesDir, ca.src)
fileInfo, err := pj.Stat(relPath)
if err != nil {
return StdoutFailed, "failed to stat relative path", err
}
if fileInfo.IsDir() {
if err := ca.copyRelativeDir(ctx, pj, relPath, conn); err != nil {
return StdoutFailed, "failed to copy relative dir", err
}
return StdoutSuccess, "", nil
}
// Handle single file
data, err := pj.ReadFile(relPath)
if err != nil {
return StdoutFailed, "failed to read relative file", err
}
if err := ca.copyFile(ctx, data, fileInfo.Mode(), conn); err != nil {
return StdoutFailed, "failed to copy relative file", err
}
return StdoutSuccess, "", nil
}
// copyAbsoluteDir copies all files from an absolute directory to the remote host.
func (ca copyArgs) copyAbsoluteDir(ctx context.Context, conn connector.Connector) (resRrr error) {
defer func() {
resRrr = ca.ensureDestDirMode(ctx, conn)
}()
return filepath.WalkDir(ca.src, func(path string, d fs.DirEntry, err error) error {
// Only copy files, skip directories
if d.IsDir() {
return nil
}
if err != nil {
return errors.WithStack(err)
}
// get file old mode
info, err := d.Info()
if err != nil {
return errors.Wrapf(err, "failed to get file %q info", path)
}
mode := info.Mode()
// read file
data, err := os.ReadFile(path)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", path)
}
// copy file to remote
rel, err := filepath.Rel(ca.src, path)
if err != nil {
return errors.Wrap(err, "failed to get relative filepath")
}
dest := filepath.Join(ca.dest, rel)
tmpDest := filepath.Join("/tmp", dest)
if err = conn.PutFile(ctx, data, tmpDest, mode); err != nil {
return err
}
_, _, err = conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(dest), tmpDest, dest))
return err
})
}
// copyRelativeDir copies all files from a relative directory (in the project) to the remote host.
func (ca copyArgs) copyRelativeDir(ctx context.Context, pj project.Project, relPath string, conn connector.Connector) (resRrr error) {
defer func() {
resRrr = ca.ensureDestDirMode(ctx, conn)
}()
return pj.WalkDir(relPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Only copy files, skip directories
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return errors.Wrap(err, "failed to get file info")
}
mode := info.Mode()
data, err := pj.ReadFile(path)
if err != nil {
return errors.Wrap(err, "failed to read file")
}
rel, err := pj.Rel(relPath, path)
if err != nil {
return errors.Wrap(err, "failed to get relative file path")
}
dest := filepath.Join(ca.dest, rel)
tmpDest := filepath.Join("/tmp", dest)
err = conn.PutFile(ctx, data, tmpDest, mode)
if err != nil {
return err
}
_, _, err = conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(dest), tmpDest, dest))
return err
})
}
// ensureDestDirMode if mode args exists, ensure dest dir mode after all files copied
func (ca copyArgs) ensureDestDirMode(ctx context.Context, conn connector.Connector) error {
if ca.mode != nil {
_, _, err := conn.ExecuteCommand(ctx, fmt.Sprintf("chmod %04o %s", *ca.mode, ca.dest))
if err != nil {
return err
}
}
return nil
}
// copyContent converts the content param and copies it to the destination file on the remote host.
func (ca copyArgs) copyContent(ctx context.Context, mode fs.FileMode, conn connector.Connector) (string, string, error) {
// Content must be copied to a file, not a directory
if strings.HasSuffix(ca.dest, "/") {
return StdoutFailed, StderrUnsupportArgs, errors.New("\"content\" should copy to a file")
}
if ca.mode != nil {
mode = os.FileMode(*ca.mode)
}
tmpDest := filepath.Join("/tmp", ca.dest)
if err := conn.PutFile(ctx, []byte(ca.content), tmpDest, mode); err != nil {
return StdoutFailed, "failed to copy file", err
}
_, _, err := conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(ca.dest), tmpDest, ca.dest))
if err != nil {
return StdoutFailed, "failed to copy file", err
}
return StdoutSuccess, "", nil
}
// copyFile copies a file (data) to the destination on the remote host.
// If the destination is a directory, the file is placed inside it with its base name.
func (ca copyArgs) copyFile(ctx context.Context, data []byte, mode fs.FileMode, conn connector.Connector) error {
dest := ca.dest
if strings.HasSuffix(ca.dest, "/") {
dest = filepath.Join(ca.dest, filepath.Base(ca.src))
}
if ca.mode != nil {
mode = os.FileMode(*ca.mode)
}
tmpDest := filepath.Join("/tmp", dest)
if err := conn.PutFile(ctx, data, tmpDest, mode); err != nil {
return err
}
_, _, err := conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(dest), tmpDest, dest))
return err
}
// Register the "copy" module at init.
func init() {
utilruntime.Must(RegisterModule("copy", ModuleCopy))
}