diff --git a/confs/repko.yaml b/confs/repko.yaml index b896762..2100f38 100644 --- a/confs/repko.yaml +++ b/confs/repko.yaml @@ -159,9 +159,6 @@ sections: variable-name: "is_secure" field-type: "check" description: "Использовать SSL" - validate: - - rule: "min(1,nginx_port)" - message: "Значение слишком большое" - name: "server_address" variable-name: "server_address" field-type: "input" @@ -191,6 +188,9 @@ sections: condition: "ssl_type=='path now'" parent: - "ssl_type" + validate: + - rule: "file_exists(ssl_cert_path)" + message: "Файл не существует" - name: "ssl_key_path" variable-name: "ssl_key_path" field-type: "file" @@ -200,6 +200,9 @@ sections: condition: "ssl_type=='path now'" parent: - "ssl_type" + validate: + - rule: "file_exists(ssl_key_path)" + message: "Файл не существует" actions: - name: "create_ssl_repko_dir" type: "command" diff --git a/setup/condition.go b/setup/condition.go index 6ac0c05..edeeeaa 100644 --- a/setup/condition.go +++ b/setup/condition.go @@ -1,6 +1,7 @@ package setup import ( + "bubble/setup/utils" "bubble/types" "errors" "fmt" @@ -174,6 +175,45 @@ func Recompute(app *types.PackageApplication) { } } +func ValidateCheckField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) types.BoolValidateFunc { + if len(validations) == 0 { + return nil + } + return func(checked bool) error { + env := ParamsForCompute(app) + env[variableName] = checked + options := append([]expr.Option{ + expr.Env(env), + expr.AllowUndefinedVariables(), + }, DefineExprCustomFunctions()...) + for _, validate := range validations { + program, err := expr.Compile(validate.Rule, options...) + prefix := fmt.Sprintf("Валидация поля %s,", fieldName) + if err != nil { + msg := fmt.Errorf("ошибка компиляции правила: %s, err: %v", validate.Rule, err) + logrus.Errorf("%s %v", prefix, msg) + return msg + } + result, err := expr.Run(program, env) + if err != nil { + msg := fmt.Errorf("ошибка выполнения правила: %s, err: %v", validate.Rule, err) + logrus.Errorf("%s %v", prefix, msg) + return msg + } + if boolValue, ok := result.(bool); ok && !boolValue { + return fmt.Errorf("%s", validate.Message) + } else if !ok { + msg := fmt.Errorf("возвращаемое значение правила: %s не булево", validate.Rule) + logrus.Errorf("%s %v", prefix, msg) + return msg + } + } + return nil + + } + +} + // ValidateField создает функцию типа textinput.ValidateFunc для проверки значения полей func ValidateField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) textinput.ValidateFunc { if len(validations) == 0 { @@ -181,7 +221,7 @@ func ValidateField(fieldName, variableName string, validations []types.FieldVali } return func(text string) error { env := ParamsForCompute(app) - env[variableName] = text //todo text это строка, если использовать функцию типа textinput.ValidateFunc, то словим ошибку типов + env[variableName] = text options := append([]expr.Option{ expr.Env(env), expr.AllowUndefinedVariables(), @@ -396,3 +436,27 @@ func GetSystemUsers() []string { } return users } + +func ExecuteAction(action *types.SectionAction, application *types.PackageApplication) error { + vars := ParamsForCondition(application) + switch action.Type { + case "command": + cmdStr, cmdErr := utils.RenderTemplate(action.Command, vars) + if cmdErr != nil { + return fmt.Errorf("ошибка создания шаблона команды: %v", cmdErr) + } + return utils.RunCommand(cmdStr, action.RunAs) + case "file_template": + source, sourceErr := utils.RenderTemplate(action.FileSource, vars) + if sourceErr != nil { + return fmt.Errorf("ошибка создания шаблона файла источника: %v", sourceErr) + } + target, targetErr := utils.RenderTemplate(action.FileTarget, vars) + if targetErr != nil { + return fmt.Errorf("ошибка создания шаблона файла цели: %v", targetErr) + } + return utils.RenderFileTemplate(source, target, vars) + default: + return fmt.Errorf("неизвестный тип действия") + } +} diff --git a/setup/fields/checkbox.go b/setup/fields/checkbox.go index 99d191e..11350c7 100644 --- a/setup/fields/checkbox.go +++ b/setup/fields/checkbox.go @@ -1,10 +1,9 @@ package fields import ( + "bubble/types" "fmt" - "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "strconv" ) // CheckboxModel структура описывающая модель выбора из двух состояний да и нет @@ -12,20 +11,19 @@ type CheckboxModel struct { label string //Имя поля description string //Описание поля checked bool //Выбранное состояние - validate textinput.ValidateFunc //Функция валидации + validate types.BoolValidateFunc //Функция валидации err error //Поле ошибки валидации } // NewCheckboxModel создаем новую модель выбора -func NewCheckboxModel(label, description string, checked bool, validator textinput.ValidateFunc) *CheckboxModel { +func NewCheckboxModel(label, description string, checked bool, validator types.BoolValidateFunc) *CheckboxModel { checkbox := &CheckboxModel{ label: label, description: description, validate: validator, checked: checked, } - checkbox.err = validator(strconv.FormatBool(checked)) - + checkbox.CheckboxValidate() return checkbox } @@ -42,7 +40,7 @@ func (m *CheckboxModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) { case " ", "enter": //Если был нажат enter или space, то меняем текущее значение на противоположное m.checked = !m.checked - m.err = m.validate(strconv.FormatBool(m.checked)) + m.CheckboxValidate() } } return m, nil @@ -83,3 +81,9 @@ func (m *CheckboxModel) GetLabel() string { func (m *CheckboxModel) GetValue() interface{} { return m.checked } + +func (m *CheckboxModel) CheckboxValidate() { + if m.validate != nil { + m.err = m.validate(m.checked) + } +} diff --git a/setup/fields/select.go b/setup/fields/select.go index 7b22179..3f15863 100644 --- a/setup/fields/select.go +++ b/setup/fields/select.go @@ -16,11 +16,21 @@ type SelectModel struct { open bool //Открыт ли список chosen string //Выбранный элемент optionsCount int //Количество элементов к показу + validate textinput.ValidateFunc + err error } // NewSelectModel создаем новую поля выбора опции -func NewSelectModel(label, value, description string, options []string) *SelectModel { +func NewSelectModel(label, value, description string, options []string, validator textinput.ValidateFunc) *SelectModel { //Проверка, что указанный по умолчанию элемент есть в списке опции + selectModel := &SelectModel{ + options: options, + label: label, + description: description, + focus: len(options), + validate: validator, + optionsCount: 10, + } found := false for i := range options { if options[i] == value { @@ -30,14 +40,9 @@ func NewSelectModel(label, value, description string, options []string) *SelectM if !found { value = "" } - return &SelectModel{ - options: options, - label: label, - description: description, - focus: len(options), - chosen: value, - optionsCount: 10, - } + selectModel.chosen = value + selectModel.SelectValidate() + return selectModel } func (m *SelectModel) Init() tea.Cmd { @@ -75,6 +80,7 @@ func (m *SelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) { m.chosen = m.options[m.focus] m.open = false m.focus = 0 + m.SelectValidate() return m, nil } } else { @@ -100,7 +106,9 @@ func (m *SelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) { func (m *SelectModel) View() string { res := fmt.Sprintf("%s: %s", m.label, m.chosen) - if m.description != "" { + if m.err != nil && !m.open { + res += fmt.Sprintf("\nОшибка валидации: %s", m.err) + } else if m.description != "" { res += fmt.Sprintf("\n%s", m.description) } if m.open && len(m.options) > 0 { @@ -141,3 +149,9 @@ func (m *SelectModel) GetLabel() string { func (m *SelectModel) GetValue() interface{} { return m.chosen } + +func (m *SelectModel) SelectValidate() { + if m.validate != nil { + m.err = m.validate(m.chosen) + } +} diff --git a/setup/form.go b/setup/form.go index c1c42c9..a401b24 100644 --- a/setup/form.go +++ b/setup/form.go @@ -47,7 +47,8 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt } } selected := fmt.Sprintf("%v", sectionField.Variable.Value) - fieldModels = append(fieldModels, fields.NewSelectModel(sectionField.Name, selected, sectionField.Description, options)) + validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application) + fieldModels = append(fieldModels, fields.NewSelectModel(sectionField.Name, selected, sectionField.Description, options, validationFunc)) case "check": checked := false if value, ok := sectionField.Variable.Value.(bool); ok { @@ -55,7 +56,7 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt } else { checked = sectionField.Variable.Value == "true" } - validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application) + validationFunc := ValidateCheckField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application) fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, sectionField.Description, checked, validationFunc)) case "compute": value, err := ComputeValue(sectionField.Compute, application) diff --git a/setup/main.go b/setup/main.go index e43b136..d3756df 100644 --- a/setup/main.go +++ b/setup/main.go @@ -94,6 +94,7 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case types.NextSectionMsg: + // todo запуск действий //Получаем сообщение о переходе на следующую секцию настраиваемого приложения. //Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны. //Возвращаем модальное окно с выходом к списку приложений diff --git a/setup/utils/utils.go b/setup/utils/utils.go index 3df6c23..d8a7c49 100644 --- a/setup/utils/utils.go +++ b/setup/utils/utils.go @@ -1,5 +1,19 @@ package utils +import ( + "bytes" + "fmt" + "github.com/sirupsen/logrus" + "html/template" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + // CalculateSelectOptionsIndex рассчитывает индекс первой и последней опции для отображения в поле выбора func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, int) { //Если количество отображаемых больше, чем есть количество опций на самом деле, @@ -29,3 +43,96 @@ func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, in } return start, end } + +func RenderFileTemplate(source, target string, vars map[string]interface{}) error { + if source == "" || target == "" { + return fmt.Errorf("source или target пустые") + } + var data []byte + var err error + if strings.HasPrefix(source, "http") { + resp, err := http.Get(source) + if err != nil { + return fmt.Errorf("ошибка получения данных файла по адресу %s, err : %v", source, err) + } + defer resp.Body.Close() + data, err = io.ReadAll(resp.Body) + } else { + data, err = os.ReadFile(source) + } + if err != nil { + return fmt.Errorf("ошибка чтения данных файла %s, err: %v", source, err) + } + tmpl, err := template.New("").Parse(string(data)) + if err != nil { + return fmt.Errorf("ошибка формирования шаблона строки: %v", err) + } + var rendered bytes.Buffer + if err = tmpl.Execute(&rendered, vars); err != nil { + return fmt.Errorf("ошибка подстановки значений в шаблон") + } + if _, err = os.Stat(target); err == nil { + backup := target + ".bak" + time.Now().Format("20060102-150405") + if cpErr := CopyFile(target, backup); cpErr != nil { + logrus.Warnf("Не удалось создать бекап: %v", cpErr) + } else { + logrus.Infof("Бекап %s создан", backup) + } + } + if err = os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return fmt.Errorf("ошибка создания директории для файла %s, err: %v", target, err) + } + if err = os.WriteFile(target, rendered.Bytes(), 0644); err != nil { + return fmt.Errorf("ошибка записи в файл %s, err: %v", target, err) + } + return nil +} + +func RenderTemplate(tmpl string, data map[string]interface{}) (string, error) { + if tmpl == "" { + return "", nil + } + t, err := template.New("").Parse(tmpl) + if err != nil { + return "", fmt.Errorf("ошибка формирования шаблона строки: %v", err) + } + var buf bytes.Buffer + if err = t.Execute(&buf, data); err != nil { + return "", fmt.Errorf("ошибка подстановки значений в шаблон: %v", err) + } + return buf.String(), nil +} + +func RunCommand(cmdStr, runAs string) error { + if cmdStr == "" { + return nil + } + var cmd *exec.Cmd + if runAs != "" && runAs != "root" { + cmd = exec.Command("su", "-c", cmdStr, runAs) + } else { + cmd = exec.Command("bach", "-c", cmdStr) + } + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("ошибка команды: %v\nВывод: %s", err, string(output)) + } + return nil +} + +// CopyFile копирование файла в новое место +func CopyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + return err +} diff --git a/types/types.go b/types/types.go index e62a380..61ad43c 100644 --- a/types/types.go +++ b/types/types.go @@ -75,3 +75,6 @@ type SectionAction struct { RequirePackages []string `yaml:"require-packages"` //Требуемые пакеты Error string `yaml:"error"` //Ошибка выполнения } + +// BoolValidateFunc функция, которая возвращает ошибку, если данные checkbox неверны +type BoolValidateFunc func(bool) error