// 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 }