97 lines
3.9 KiB
Go
97 lines
3.9 KiB
Go
// Package module определяет единственный контракт, которому подчиняются
|
||
// и встроенные (builtin, чистый Go), и внешние (external, subprocess+JSON)
|
||
// модули. Executor не знает разницы между ними — он просто резолвит
|
||
// имя модуля через Registry и вызывает Run.
|
||
package module
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/vladimir/goherence/internal/ssh"
|
||
)
|
||
|
||
// Input — то, что модуль получает на вход. Для builtin-модулей это просто
|
||
// структура в памяти; для external-модулей ровно эти же поля сериализуются
|
||
// в JSON и уходят в stdin subprocess'а — контракт единый и явный.
|
||
type Input struct {
|
||
SchemaVersion int `json:"schema_version"`
|
||
Args map[string]interface{} `json:"args"`
|
||
Host string `json:"host"`
|
||
Become bool `json:"become"`
|
||
CheckMode bool `json:"check_mode"`
|
||
}
|
||
|
||
// Result — то, что модуль возвращает после выполнения.
|
||
type Result struct {
|
||
SchemaVersion int `json:"schema_version"`
|
||
Changed bool `json:"changed"`
|
||
Failed bool `json:"failed"`
|
||
Msg string `json:"msg"`
|
||
Diff interface{} `json:"diff,omitempty"`
|
||
}
|
||
|
||
// SchemaVersion — текущая версия контракта Input/Result. Меняется только
|
||
// при несовместимых изменениях формата; внешние модули должны проверять
|
||
// это поле и явно отказываться работать с версией, которую не понимают.
|
||
const SchemaVersion = 1
|
||
|
||
// Module — контракт, общий для builtin-функций и обёрток над subprocess.
|
||
type Module interface {
|
||
Name() string
|
||
Run(ctx context.Context, in Input, conn *ssh.Conn) (Result, error)
|
||
}
|
||
|
||
// Registry хранит все известные модули и резолвит их по имени.
|
||
// Порядок поиска: сперва builtin (быстрее, без overhead процесса),
|
||
// затем external — так что локальный модуль с тем же именем, что и
|
||
// встроенный, никогда не переопределяет его молча.
|
||
type Registry struct {
|
||
builtin map[string]Module
|
||
external map[string]Module // заполняется отдельно, см. internal/module/external
|
||
}
|
||
|
||
func NewRegistry() *Registry {
|
||
return &Registry{
|
||
builtin: map[string]Module{},
|
||
external: map[string]Module{},
|
||
}
|
||
}
|
||
|
||
func (r *Registry) RegisterBuiltin(m Module) {
|
||
r.builtin[m.Name()] = m
|
||
}
|
||
|
||
func (r *Registry) RegisterExternal(m Module) {
|
||
r.external[m.Name()] = m
|
||
}
|
||
|
||
// Resolve возвращает модуль по имени. Принимает как короткие имена
|
||
// (`shell`), так и полные (`ansible.builtin.shell`) — второй компонент
|
||
// после последней точки трактуется как основное имя.
|
||
func (r *Registry) Resolve(name string) (Module, error) {
|
||
short := shortName(name)
|
||
if m, ok := r.builtin[short]; ok {
|
||
return m, nil
|
||
}
|
||
if m, ok := r.external[short]; ok {
|
||
return m, nil
|
||
}
|
||
return nil, ErrModuleNotFound{Name: name}
|
||
}
|
||
|
||
func shortName(name string) string {
|
||
for i := len(name) - 1; i >= 0; i-- {
|
||
if name[i] == '.' {
|
||
return name[i+1:]
|
||
}
|
||
}
|
||
return name
|
||
}
|
||
|
||
// ErrModuleNotFound — отдельный тип ошибки, чтобы executor мог отличить
|
||
// "модуль не найден" от прочих ошибок выполнения и дать понятную подсказку.
|
||
type ErrModuleNotFound struct{ Name string }
|
||
|
||
func (e ErrModuleNotFound) Error() string {
|
||
return "модуль не найден: " + e.Name
|
||
}
|