goherence/internal/module/module.go
2026-09-11 10:17:25 +03:00

97 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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