Skip to content

Commit

Permalink
add chmod-x options to copy command
Browse files Browse the repository at this point in the history
  • Loading branch information
umputun committed Nov 4, 2023
1 parent 9834ec7 commit 9de5d06
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 2 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ Read more about YAML multiline string formatting on [yaml-multiline.info](https:
Copies a file from the local machine to the remote host(s). If `mkdir` is set to `true` the command will create the destination directory if it doesn't exist, the same as `mkdir -p` in bash. The command also supports glob patterns in `src` field.

Copy command performs a quick check to see if the file already exists on the remote host(s) with the same size and modification time,
and skips the copy if it does. This option can be disabled by setting `force: true` flag. Another option is `exclude` which allows to specify a list of files to exclude to be copied.
and skips the copy if it does. This option can be disabled by setting `force: true` flag. Another option is `exclude` which allows to specify a list of files to exclude to be copied.


```yaml
- name: copy file with mkdir
Expand All @@ -366,6 +367,15 @@ Copy also supports list format to copy multiple files at once:
- {"src": "testdata/*.yml", "dst": "/tmp/things"}
```

Copy file and making it executable is also supported:

```yaml
- name: copy file and make it executable
copy:
- {"src": "testdata/script.sh", "dst": "/tmp/script.sh", "chmod_x": true}
```


#### `sync`

Synchronises directory from the local machine to the remote host(s). Optionally supports deleting files on the remote host(s) that don't exist locally with `"delete": true` flag. Another option is `exclude` which allows to specify a list of files to exclude from the sync.
Expand Down
1 change: 1 addition & 0 deletions pkg/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type CopyInternal struct {
Mkdir bool `yaml:"mkdir" toml:"mkdir"` // create destination directory if it does not exist
Force bool `yaml:"force" toml:"force"` // force copy even if source and destination are the same
Exclude []string `yaml:"exclude" toml:"exclude"` // exclude files matching these patterns
ChmodX bool `yaml:"chmod_x" toml:"chmod_x"` // chmod +x on destination file
}

// SyncInternal defines sync command (recursive copy), implemented internally
Expand Down
15 changes: 14 additions & 1 deletion pkg/runner/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ func (ec *execCmd) Copy(ctx context.Context) (resp execCmdResp, err error) {
if err := ec.exec.Upload(ctx, src, dst, opts); err != nil {
return resp, ec.errorFmt("can't copy file to %s: %w", ec.hostAddr, err)
}
if ec.cmd.Copy.ChmodX {
if _, err := ec.exec.Run(ctx, fmt.Sprintf("chmod +x %s", dst), &executor.RunOpts{Verbose: ec.verbose}); err != nil {
return resp, ec.errorFmt("can't chmod +x file on %s: %w", ec.hostAddr, err)
}
resp.details = fmt.Sprintf(" {copy: %s -> %s, chmod: +x}", src, dst)
}
return resp, nil
}

Expand Down Expand Up @@ -164,6 +170,12 @@ func (ec *execCmd) Copy(ctx context.Context) (resp execCmdResp, err error) {
return resp, ec.errorFmt("can't move file to %s: %w", ec.hostAddr, err)
}
}
if ec.cmd.Copy.ChmodX {
if _, err := ec.exec.Run(ctx, fmt.Sprintf("sudo chmod +x %s", dst), &executor.RunOpts{Verbose: ec.verbose}); err != nil {
return resp, ec.errorFmt("can't chmod +x file on %s: %w", ec.hostAddr, err)
}
resp.details = fmt.Sprintf(" {copy: %s -> %s, sudo: true, chmod: +x}", src, dst)
}
}

return resp, nil
Expand All @@ -178,7 +190,8 @@ func (ec *execCmd) Mcopy(ctx context.Context) (resp execCmdResp, err error) {
dst := tmpl.apply(c.Dest)
msgs = append(msgs, fmt.Sprintf("%s -> %s", src, dst))
ecSingle := ec
ecSingle.cmd.Copy = config.CopyInternal{Source: src, Dest: dst, Mkdir: c.Mkdir, Force: c.Force}
ecSingle.cmd.Copy = config.CopyInternal{Source: src, Dest: dst, Mkdir: c.Mkdir, Force: c.Force,
ChmodX: c.ChmodX, Exclude: c.Exclude}
if _, err := ecSingle.Copy(ctx); err != nil {
return resp, ec.errorFmt("can't copy file to %s: %w", ec.hostAddr, err)
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/runner/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,41 @@ func Test_execCmdWithTmp(t *testing.T) {
assert.Contains(t, wr.String(), fmt.Sprintf("cannot access '%s'", tmpPath))
})

t.Run("copy a single file with sudo and chmod+x", func(t *testing.T) {
wr := bytes.NewBuffer(nil)
log.SetOutput(io.MultiWriter(wr, os.Stdout))

ec := execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{
Options: config.CmdOptions{Sudo: true},
Copy: config.CopyInternal{Source: "testdata/inventory.yml", Dest: "/tmp/inventory.txt", ChmodX: true}}}
resp, err := ec.Copy(ctx)
require.NoError(t, err)
assert.Equal(t, " {copy: testdata/inventory.yml -> /tmp/inventory.txt, sudo: true, chmod: +x}", resp.details)
tmpPath := extractTmpPath(wr.String())
assert.NotEmpty(t, tmpPath)
t.Logf("tmpPath: %s", tmpPath)

// check if dest contains file
wr.Reset()
ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{
Script: "ls -la /tmp/inventory.txt"},
}
resp, err = ec.Script(ctx)
require.NoError(t, err)
assert.Contains(t, wr.String(), "/tmp/inventory.txt")
assert.Contains(t, wr.String(), "> -rwxr-xr-x ", "file should be executable")

// check if tmp dir removed
wr.Reset()
ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{
Options: config.CmdOptions{Sudo: true},
Script: "ls -la " + tmpPath},
}
resp, err = ec.Script(ctx)
require.Error(t, err)
assert.Contains(t, wr.String(), fmt.Sprintf("cannot access '%s'", tmpPath))
})

t.Run("copy multiple files with sudo", func(t *testing.T) {
wr := bytes.NewBuffer(nil)
log.SetOutput(io.MultiWriter(wr, os.Stdout))
Expand Down

0 comments on commit 9de5d06

Please sign in to comment.