Работа над валидацией полей, работа над выполнением действий
This commit is contained in:
parent
e37cabadcc
commit
dc263b76bf
8 changed files with 220 additions and 23 deletions
|
|
@ -159,9 +159,6 @@ sections:
|
||||||
variable-name: "is_secure"
|
variable-name: "is_secure"
|
||||||
field-type: "check"
|
field-type: "check"
|
||||||
description: "Использовать SSL"
|
description: "Использовать SSL"
|
||||||
validate:
|
|
||||||
- rule: "min(1,nginx_port)"
|
|
||||||
message: "Значение слишком большое"
|
|
||||||
- name: "server_address"
|
- name: "server_address"
|
||||||
variable-name: "server_address"
|
variable-name: "server_address"
|
||||||
field-type: "input"
|
field-type: "input"
|
||||||
|
|
@ -191,6 +188,9 @@ sections:
|
||||||
condition: "ssl_type=='path now'"
|
condition: "ssl_type=='path now'"
|
||||||
parent:
|
parent:
|
||||||
- "ssl_type"
|
- "ssl_type"
|
||||||
|
validate:
|
||||||
|
- rule: "file_exists(ssl_cert_path)"
|
||||||
|
message: "Файл не существует"
|
||||||
- name: "ssl_key_path"
|
- name: "ssl_key_path"
|
||||||
variable-name: "ssl_key_path"
|
variable-name: "ssl_key_path"
|
||||||
field-type: "file"
|
field-type: "file"
|
||||||
|
|
@ -200,6 +200,9 @@ sections:
|
||||||
condition: "ssl_type=='path now'"
|
condition: "ssl_type=='path now'"
|
||||||
parent:
|
parent:
|
||||||
- "ssl_type"
|
- "ssl_type"
|
||||||
|
validate:
|
||||||
|
- rule: "file_exists(ssl_key_path)"
|
||||||
|
message: "Файл не существует"
|
||||||
actions:
|
actions:
|
||||||
- name: "create_ssl_repko_dir"
|
- name: "create_ssl_repko_dir"
|
||||||
type: "command"
|
type: "command"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package setup
|
package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bubble/setup/utils"
|
||||||
"bubble/types"
|
"bubble/types"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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 для проверки значения полей
|
// ValidateField создает функцию типа textinput.ValidateFunc для проверки значения полей
|
||||||
func ValidateField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) textinput.ValidateFunc {
|
func ValidateField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) textinput.ValidateFunc {
|
||||||
if len(validations) == 0 {
|
if len(validations) == 0 {
|
||||||
|
|
@ -181,7 +221,7 @@ func ValidateField(fieldName, variableName string, validations []types.FieldVali
|
||||||
}
|
}
|
||||||
return func(text string) error {
|
return func(text string) error {
|
||||||
env := ParamsForCompute(app)
|
env := ParamsForCompute(app)
|
||||||
env[variableName] = text //todo text это строка, если использовать функцию типа textinput.ValidateFunc, то словим ошибку типов
|
env[variableName] = text
|
||||||
options := append([]expr.Option{
|
options := append([]expr.Option{
|
||||||
expr.Env(env),
|
expr.Env(env),
|
||||||
expr.AllowUndefinedVariables(),
|
expr.AllowUndefinedVariables(),
|
||||||
|
|
@ -396,3 +436,27 @@ func GetSystemUsers() []string {
|
||||||
}
|
}
|
||||||
return users
|
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("неизвестный тип действия")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package fields
|
package fields
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bubble/types"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckboxModel структура описывающая модель выбора из двух состояний да и нет
|
// CheckboxModel структура описывающая модель выбора из двух состояний да и нет
|
||||||
|
|
@ -12,20 +11,19 @@ type CheckboxModel struct {
|
||||||
label string //Имя поля
|
label string //Имя поля
|
||||||
description string //Описание поля
|
description string //Описание поля
|
||||||
checked bool //Выбранное состояние
|
checked bool //Выбранное состояние
|
||||||
validate textinput.ValidateFunc //Функция валидации
|
validate types.BoolValidateFunc //Функция валидации
|
||||||
err error //Поле ошибки валидации
|
err error //Поле ошибки валидации
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCheckboxModel создаем новую модель выбора
|
// 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{
|
checkbox := &CheckboxModel{
|
||||||
label: label,
|
label: label,
|
||||||
description: description,
|
description: description,
|
||||||
validate: validator,
|
validate: validator,
|
||||||
checked: checked,
|
checked: checked,
|
||||||
}
|
}
|
||||||
checkbox.err = validator(strconv.FormatBool(checked))
|
checkbox.CheckboxValidate()
|
||||||
|
|
||||||
return checkbox
|
return checkbox
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,7 +40,7 @@ func (m *CheckboxModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
case " ", "enter":
|
case " ", "enter":
|
||||||
//Если был нажат enter или space, то меняем текущее значение на противоположное
|
//Если был нажат enter или space, то меняем текущее значение на противоположное
|
||||||
m.checked = !m.checked
|
m.checked = !m.checked
|
||||||
m.err = m.validate(strconv.FormatBool(m.checked))
|
m.CheckboxValidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -83,3 +81,9 @@ func (m *CheckboxModel) GetLabel() string {
|
||||||
func (m *CheckboxModel) GetValue() interface{} {
|
func (m *CheckboxModel) GetValue() interface{} {
|
||||||
return m.checked
|
return m.checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *CheckboxModel) CheckboxValidate() {
|
||||||
|
if m.validate != nil {
|
||||||
|
m.err = m.validate(m.checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,21 @@ type SelectModel struct {
|
||||||
open bool //Открыт ли список
|
open bool //Открыт ли список
|
||||||
chosen string //Выбранный элемент
|
chosen string //Выбранный элемент
|
||||||
optionsCount int //Количество элементов к показу
|
optionsCount int //Количество элементов к показу
|
||||||
|
validate textinput.ValidateFunc
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSelectModel создаем новую поля выбора опции
|
// 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
|
found := false
|
||||||
for i := range options {
|
for i := range options {
|
||||||
if options[i] == value {
|
if options[i] == value {
|
||||||
|
|
@ -30,14 +40,9 @@ func NewSelectModel(label, value, description string, options []string) *SelectM
|
||||||
if !found {
|
if !found {
|
||||||
value = ""
|
value = ""
|
||||||
}
|
}
|
||||||
return &SelectModel{
|
selectModel.chosen = value
|
||||||
options: options,
|
selectModel.SelectValidate()
|
||||||
label: label,
|
return selectModel
|
||||||
description: description,
|
|
||||||
focus: len(options),
|
|
||||||
chosen: value,
|
|
||||||
optionsCount: 10,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SelectModel) Init() tea.Cmd {
|
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.chosen = m.options[m.focus]
|
||||||
m.open = false
|
m.open = false
|
||||||
m.focus = 0
|
m.focus = 0
|
||||||
|
m.SelectValidate()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -100,7 +106,9 @@ func (m *SelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
|
|
||||||
func (m *SelectModel) View() string {
|
func (m *SelectModel) View() string {
|
||||||
res := fmt.Sprintf("%s: %s", m.label, m.chosen)
|
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)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
if m.open && len(m.options) > 0 {
|
if m.open && len(m.options) > 0 {
|
||||||
|
|
@ -141,3 +149,9 @@ func (m *SelectModel) GetLabel() string {
|
||||||
func (m *SelectModel) GetValue() interface{} {
|
func (m *SelectModel) GetValue() interface{} {
|
||||||
return m.chosen
|
return m.chosen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *SelectModel) SelectValidate() {
|
||||||
|
if m.validate != nil {
|
||||||
|
m.err = m.validate(m.chosen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selected := fmt.Sprintf("%v", sectionField.Variable.Value)
|
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":
|
case "check":
|
||||||
checked := false
|
checked := false
|
||||||
if value, ok := sectionField.Variable.Value.(bool); ok {
|
if value, ok := sectionField.Variable.Value.(bool); ok {
|
||||||
|
|
@ -55,7 +56,7 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
} else {
|
} else {
|
||||||
checked = sectionField.Variable.Value == "true"
|
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))
|
fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, sectionField.Description, checked, validationFunc))
|
||||||
case "compute":
|
case "compute":
|
||||||
value, err := ComputeValue(sectionField.Compute, application)
|
value, err := ComputeValue(sectionField.Compute, application)
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case types.NextSectionMsg:
|
case types.NextSectionMsg:
|
||||||
|
// todo запуск действий
|
||||||
//Получаем сообщение о переходе на следующую секцию настраиваемого приложения.
|
//Получаем сообщение о переходе на следующую секцию настраиваемого приложения.
|
||||||
//Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны.
|
//Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны.
|
||||||
//Возвращаем модальное окно с выходом к списку приложений
|
//Возвращаем модальное окно с выходом к списку приложений
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,19 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// CalculateSelectOptionsIndex рассчитывает индекс первой и последней опции для отображения в поле выбора
|
// CalculateSelectOptionsIndex рассчитывает индекс первой и последней опции для отображения в поле выбора
|
||||||
func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, int) {
|
func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, int) {
|
||||||
//Если количество отображаемых больше, чем есть количество опций на самом деле,
|
//Если количество отображаемых больше, чем есть количество опций на самом деле,
|
||||||
|
|
@ -29,3 +43,96 @@ func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, in
|
||||||
}
|
}
|
||||||
return start, end
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,6 @@ type SectionAction struct {
|
||||||
RequirePackages []string `yaml:"require-packages"` //Требуемые пакеты
|
RequirePackages []string `yaml:"require-packages"` //Требуемые пакеты
|
||||||
Error string `yaml:"error"` //Ошибка выполнения
|
Error string `yaml:"error"` //Ошибка выполнения
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BoolValidateFunc функция, которая возвращает ошибку, если данные checkbox неверны
|
||||||
|
type BoolValidateFunc func(bool) error
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue