101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package builtin
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
"github.com/vladimir/goherence/internal/module"
|
||
"github.com/vladimir/goherence/internal/ssh"
|
||
)
|
||
|
||
// FileModule — аналог ansible.builtin.file: управляет состоянием пути
|
||
// (директория/файл/отсутствие) и правами доступа. Идемпотентен через `stat`.
|
||
type FileModule struct{}
|
||
|
||
func (m *FileModule) Name() string { return "file" }
|
||
|
||
func (m *FileModule) Run(ctx context.Context, in module.Input, conn *ssh.Conn) (module.Result, error) {
|
||
path, ok := in.Args["path"].(string)
|
||
if !ok {
|
||
return module.Result{}, fmt.Errorf("file: аргумент path обязателен")
|
||
}
|
||
state, _ := in.Args["state"].(string)
|
||
if state == "" {
|
||
state = "file"
|
||
}
|
||
mode, _ := in.Args["mode"].(string)
|
||
|
||
current, err := m.stat(conn, path)
|
||
if err != nil {
|
||
return module.Result{}, err
|
||
}
|
||
|
||
changed := false
|
||
var action string
|
||
|
||
switch state {
|
||
case "directory":
|
||
if current != "directory" {
|
||
action = fmt.Sprintf("mkdir -p %s", shArg(path))
|
||
changed = true
|
||
}
|
||
case "absent":
|
||
if current != "absent" {
|
||
action = fmt.Sprintf("rm -rf %s", shArg(path))
|
||
changed = true
|
||
}
|
||
case "touch":
|
||
if current == "absent" {
|
||
action = fmt.Sprintf("touch %s", shArg(path))
|
||
changed = true
|
||
}
|
||
default:
|
||
return module.Result{}, fmt.Errorf("file: неподдерживаемый state %q (первая версия: directory/absent/touch)", state)
|
||
}
|
||
|
||
if mode != "" && current != "absent" {
|
||
if action != "" {
|
||
action += " && "
|
||
}
|
||
action += fmt.Sprintf("chmod %s %s", mode, shArg(path))
|
||
changed = true // не проверяем текущий режим отдельно в первой версии — упрощение
|
||
}
|
||
|
||
if !changed {
|
||
return module.Result{SchemaVersion: module.SchemaVersion, Changed: false, Msg: "уже в нужном состоянии"}, nil
|
||
}
|
||
if in.CheckMode {
|
||
return module.Result{SchemaVersion: module.SchemaVersion, Changed: true, Msg: "check mode: " + action}, nil
|
||
}
|
||
|
||
res, err := conn.Run(action)
|
||
if err != nil {
|
||
return module.Result{}, err
|
||
}
|
||
if res.ExitCode != 0 {
|
||
return module.Result{SchemaVersion: module.SchemaVersion, Failed: true,
|
||
Msg: fmt.Sprintf("команда завершилась с кодом %d: %s", res.ExitCode, res.Stderr)}, nil
|
||
}
|
||
return module.Result{SchemaVersion: module.SchemaVersion, Changed: true, Msg: action}, nil
|
||
}
|
||
|
||
func (m *FileModule) stat(conn *ssh.Conn, path string) (string, error) {
|
||
res, err := conn.Run(fmt.Sprintf(
|
||
`test -d %s && echo directory || (test -e %s && echo file || echo absent)`,
|
||
shArg(path), shArg(path)))
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
switch {
|
||
case containsTrim(res.Stdout, "directory"):
|
||
return "directory", nil
|
||
case containsTrim(res.Stdout, "absent"):
|
||
return "absent", nil
|
||
default:
|
||
return "file", nil
|
||
}
|
||
}
|
||
|
||
func containsTrim(s, sub string) bool {
|
||
return len(s) >= len(sub) && (s == sub || s == sub+"\n")
|
||
}
|