From b1bde827de38774a8f4de5c5bd93654a4f95e622 Mon Sep 17 00:00:00 2001 From: gitops Date: Mon, 4 Aug 2025 11:03:25 +0300 Subject: [PATCH] Init --- .gitignore | 45 ++++ README.md | 414 ++++++++++++++++++++++++++++++++ cmd/main.go | 120 +++++++++ config.yaml | 8 + example/main.go | 266 ++++++++++++++++++++ example/main_test.go | 337 ++++++++++++++++++++++++++ gitops_rac.go | 124 ++++++++++ go.mod | 5 + go.sum | 4 + integration_test.go | 180 ++++++++++++++ internal/config/config.go | 201 ++++++++++++++++ internal/config/config_test.go | 209 ++++++++++++++++ internal/constants/constants.go | 291 ++++++++++++++++++++++ internal/logger/logger.go | 110 +++++++++ internal/logger/logger_test.go | 238 ++++++++++++++++++ internal/rac/rac.go | 333 +++++++++++++++++++++++++ internal/rac/service_mode.go | 235 ++++++++++++++++++ internal/service/service.go | 124 ++++++++++ project.yaml | 16 ++ requirements.md | 319 ++++++++++++++++++++++++ 20 files changed, 3579 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/main.go create mode 100644 config.yaml create mode 100644 example/main.go create mode 100644 example/main_test.go create mode 100644 gitops_rac.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 integration_test.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/constants/constants.go create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/logger_test.go create mode 100644 internal/rac/rac.go create mode 100644 internal/rac/service_mode.go create mode 100644 internal/service/service.go create mode 100644 project.yaml create mode 100644 requirements.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64ac7ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Project specific +promts/ +secret.yaml + +# Build artifacts +dist/ +build/ + +# Logs +*.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd0dab0 --- /dev/null +++ b/README.md @@ -0,0 +1,414 @@ +# GitOps RAC - 1C Service Mode Management + +Го-библиотека и CLI-утилита для управления сервисным режимом информационных баз 1С:Предприятие через утилиту RAC (Remote Administration Client). + +## Возможности + +- ✅ Включение и выключение сервисного режима 1С +- ✅ Принудительное отключение всех пользователей при включении сервисного режима +- ✅ Верификация операций +- ✅ Поддержка retry-логики при сбоях +- ✅ Маскирование паролей в логах +- ✅ CLI интерфейс с поддержкой флагов +- ✅ **Библиотечный API для интеграции в другие проекты** +- ✅ Comprehensive logging с использованием log/slog +- ✅ Конфигурация через YAML файлы (трехфайловая структура) +- ✅ Поддержка контекстов для управления таймаутами +- ✅ Полное покрытие тестами + +## Установка + +### Как CLI утилита + +```bash +go build -o benadis-rac.exe ./cmd +``` + +### Как библиотека + +```bash +go get benadis-rac +``` + +## Быстрый старт + +### Использование как библиотека + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + benadisrac "git.benadis.ru/gitops/benadis-rac/" +) + +func main() { + // Создание клиента + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: "config.yaml", + SecretPath: "secret.yaml", + ProjectPath: "project.yaml", + LogLevel: "Info", + }) + if err != nil { + log.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Получение статуса + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Service mode status: %t\n", status) + + // Включение сервисного режима + if err := client.EnableServiceMode(ctx); err != nil { + log.Fatal(err) + } + + // Выключение сервисного режима + if err := client.DisableServiceMode(ctx); err != nil { + log.Fatal(err) + } +} +``` + +### Использование как CLI + +```bash +# Проверка статуса +./benadis-rac.exe status + +# Включение сервисного режима +./benadis-rac.exe enable + +# Выключение сервисного режима +./benadis-rac.exe disable +``` + +## Конфигурация + +Проект использует трехфайловую структуру конфигурации: + +### config.yaml - Основная конфигурация + +```yaml +# Путь к rac.exe +rac_path: "C:/Program Files/1cv8/8.3.27.1606/bin/rac.exe" + +# Таймауты +connection_timeout: "30s" +command_timeout: "60s" + +# Настройки повторных попыток +retry_count: 3 +retry_delay: "5s" +``` + +### secret.yaml - Учетные данные + +```yaml +# Глобальные учетные данные +cluster_admin: "admin" +cluster_admin_password: "password" +db_admin: "dbadmin" +db_admin_password: "dbpassword" + +# Учетные данные для конкретных проектов +projects: + project1: + cluster_admin: "project1_admin" + cluster_admin_password: "project1_password" +``` + +### project.yaml - Настройки проекта + +```yaml +service-mode: + server_host: "localhost" + server_port: 1540 + rac_port: 1545 + log_level: "Info" + server_name: "1c-server" + base_name: "test-base" + command: "enable" # enable, disable, status +``` + +### secret.yaml + +```yaml +# Настройки для аутентификации +cluster_admin: "gitops" +cluster_admin_password: "password" +db_admin: "gitops" +db_admin_password: "password" +``` + +## Использование + +### CLI интерфейс + +```bash +# Включить сервисный режим +benadis-rac -command enable + +# Выключить сервисный режим +benadis-rac -command disable + +# Проверить статус сервисного режима +benadis-rac -command status + +# Использовать кастомные пути к конфигурации +benadis-rac -config /path/to/config.yaml -secret /path/to/secret.yaml -command enable + +# Показать справку +benadis-rac -help + +# Показать версию +benadis-rac -version +``` + +### Библиотечный API + +#### Основные типы + +```go +// Config конфигурация для создания клиента +type Config struct { + ConfigPath string // Путь к файлу конфигурации + SecretPath string // Путь к файлу с секретами + ProjectPath string // Путь к файлу проекта + LogLevel string // Уровень логирования (Debug, Info, Warn, Error) +} + +// ServiceModeManager интерфейс для управления сервисным режимом +type ServiceModeManager interface { + EnableServiceMode(ctx context.Context) error + DisableServiceMode(ctx context.Context) error + GetServiceModeStatus(ctx context.Context) (bool, error) +} +``` + +#### Создание клиента + +```go +// Создание клиента с конфигурацией +client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: "config.yaml", + SecretPath: "secret.yaml", + ProjectPath: "project.yaml", + LogLevel: "Info", +}) +if err != nil { + log.Fatal(err) +} + +// Создание клиента из готовой конфигурации +appConfig, err := config.LoadConfig("config.yaml", "secret.yaml", "project.yaml") +if err != nil { + log.Fatal(err) +} +client, err := benadisrac.NewClientFromConfig(appConfig, "Debug") +if err != nil { + log.Fatal(err) +} +``` + +#### Методы клиента + +```go +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() + +// Получение статуса сервисного режима +status, err := client.GetServiceModeStatus(ctx) +if err != nil { + log.Printf("Error getting status: %v", err) +} else { + fmt.Printf("Service mode enabled: %t\n", status) +} + +// Включение сервисного режима +if err := client.EnableServiceMode(ctx); err != nil { + log.Printf("Error enabling service mode: %v", err) +} else { + fmt.Println("Service mode enabled successfully") +} + +// Выключение сервисного режима +if err := client.DisableServiceMode(ctx); err != nil { + log.Printf("Error disabling service mode: %v", err) +} else { + fmt.Println("Service mode disabled successfully") +} +``` + +#### Обработка ошибок + +```go +// Пример с обработкой различных типов ошибок +status, err := client.GetServiceModeStatus(ctx) +if err != nil { + switch { + case errors.Is(err, context.DeadlineExceeded): + log.Println("Operation timed out") + case errors.Is(err, context.Canceled): + log.Println("Operation was canceled") + default: + log.Printf("Unexpected error: %v", err) + } + return +} +``` + +#### Полный пример + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + benadisrac "git.benadis.ru/gitops/benadis-rac/" +) + +func main() { + // Создаем клиент + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: "config.yaml", + SecretPath: "secret.yaml", + ProjectPath: "project.yaml", + LogLevel: "Info", + }) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + // Включаем сервисный режим + if err := client.EnableServiceMode(ctx); err != nil { + log.Fatalf("Failed to enable service mode: %v", err) + } + + // Проверяем статус + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + log.Fatalf("Failed to get status: %v", err) + } + + fmt.Printf("Service mode enabled: %v\n", status) + + // Выключаем сервисный режим + if err := client.DisableServiceMode(ctx); err != nil { + log.Fatalf("Failed to disable service mode: %v", err) + } +} +``` + +## Архитектура + +Проект следует принципам SOLID и использует модульную архитектуру: + +``` +benadis-rac/ +├── cmd/ # CLI приложение +│ └── main.go +├── internal/ # Внутренние пакеты +│ ├── config/ # Управление конфигурацией +│ ├── constants/ # Константы +│ ├── logger/ # Логирование с маскированием паролей +│ ├── rac/ # Взаимодействие с RAC +│ └── service/ # Бизнес-логика +├── gitops_rac.go # Публичный API +├── integration_test.go # Интеграционные тесты +└── *_test.go # Unit тесты +``` + +## Тестирование + +### Unit тесты + +```bash +go test ./... +``` + +### Интеграционные тесты + +```bash +# Запуск интеграционных тестов (требует настроенной среды) +go test -tags=integration ./... + +# Запуск тестов с покрытием +go test -cover ./... + +# Запуск бенчмарков +go test -bench=. ./... +``` + +### Пример использования + +Полный рабочий пример находится в директории `example/`: + +```bash +cd example +go run main.go +``` + +## Разработка + +### Требования + +- Go 1.21+ +- Доступ к кластеру 1С:Предприятие 8 +- Настроенные файлы конфигурации + +### Сборка + +```bash +# Сборка для текущей платформы +go build -o benadis-rac ./cmd + +# Кросс-компиляция для Windows +GOOS=windows GOARCH=amd64 go build -o benadis-rac.exe ./cmd + +# Сборка с оптимизацией размера +go build -ldflags="-s -w" -o benadis-rac ./cmd +``` + +### Линтинг + +```bash +# Установка golangci-lint +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +# Запуск линтера +golangci-lint run +``` + +## Лицензия + +MIT License - см. файл LICENSE для подробностей. + +## Вклад в проект + +1. Форкните репозиторий +2. Создайте ветку для новой функции (`git checkout -b feature/amazing-feature`) +3. Зафиксируйте изменения (`git commit -m 'Add amazing feature'`) +4. Отправьте в ветку (`git push origin feature/amazing-feature`) +5. Откройте Pull Request + +## Поддержка + +Для вопросов и поддержки создайте issue в репозитории проекта. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..bcaaed8 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "time" + + "git.benadis.ru/gitops/benadis-rac/internal/config" + "git.benadis.ru/gitops/benadis-rac/internal/constants" + "git.benadis.ru/gitops/benadis-rac/internal/logger" + "git.benadis.ru/gitops/benadis-rac/internal/service" +) + +func main() { + // Определяем флаги командной строки + var ( + configPath = flag.String("config", "config.yaml", "Path to config file") + secretPath = flag.String("secret", "secret.yaml", "Path to secret file") + projectPath = flag.String("project", "project.yaml", "Path to project file") + command = flag.String("command", "", "Command to execute: enable, disable, status") + showVersion = flag.Bool("version", false, "Show version") + showHelp = flag.Bool("help", false, "Show help") + ) + + flag.Parse() + + // Показываем версию + if *showVersion { + fmt.Printf(constants.MsgVersionFormat, constants.AppVersion) + return + } + + // Показываем справку + if *showHelp || *command == "" { + showUsage() + return + } + + // Загружаем конфигурацию + cfg, err := config.LoadConfig(*configPath, *secretPath, *projectPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + + // Валидируем конфигурацию + if err := cfg.Validate(); err != nil { + fmt.Fprintf(os.Stderr, "Config validation error: %v\n", err) + os.Exit(1) + } + + // Создаем логгер + log := logger.NewLogger(cfg.Project.ServiceMode.LogLevel) + + // Создаем сервис + svc := service.NewServiceModeService(cfg, log) + + // Создаем контекст с таймаутом + ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultMainTimeout*time.Minute) + defer cancel() + + // Выполняем команду + if err := executeCommand(ctx, svc, log, *command); err != nil { + log.Error("Command execution failed", "command", *command, "error", err) + os.Exit(1) + } +} + +// executeCommand выполняет указанную команду +func executeCommand(ctx context.Context, svc *service.ServiceModeService, log logger.Logger, command string) error { + switch command { + case constants.CmdEnable: + return svc.EnableServiceMode(ctx) + case constants.CmdDisable: + return svc.DisableServiceMode(ctx) + case constants.CmdStatus: + status, err := svc.GetServiceModeStatus(ctx) + if err != nil { + return err + } + if status { + log.Info(constants.StatusServiceModeEnabled) + } else { + log.Info(constants.StatusServiceModeDisabled) + } + return nil + default: + return fmt.Errorf(constants.ErrUnknownCommand, command) + } +} + +// showUsage показывает справку по использованию +func showUsage() { + fmt.Printf(`GitOps RAC v%s - 1C Service Mode Management Tool + +Usage: + benadis-rac [options] -command + +Commands: + enable - Enable service mode + disable - Disable service mode + status - Show current service mode status + +Options: + -config Path to config file (default: config.yaml) + -secret Path to secret file (default: secret.yaml) + -project Path to project file (default: project.yaml) + -version Show version + -help Show this help + +Examples: + benadis-rac -command enable + benadis-rac -command disable + benadis-rac -command status + benadis-rac -config /path/to/config.yaml -secret /path/to/secret.yaml -project /path/to/project.yaml -command enable + +`, constants.AppVersion) +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..d119eb3 --- /dev/null +++ b/config.yaml @@ -0,0 +1,8 @@ +# Путь к rac.exe +rac_path: "C:/Program Files/1cv8/8.3.27.1606/bin/rac.exe" + +# Таймауты +connection_timeout: 30s +command_timeout: 60s +retry_count: 3 +retry_delay: 5s diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..68b7306 --- /dev/null +++ b/example/main.go @@ -0,0 +1,266 @@ +// Package main демонстрирует использование библиотеки benadis-rac +// для управления сервисным режимом 1C +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + benadisrac "git.benadis.ru/gitops/benadis-rac" +) + +// ExampleConfig содержит примеры конфигурации для демонстрации +type ExampleConfig struct { + ConfigPath string + SecretPath string + ProjectPath string + LogLevel string +} + +// getDefaultConfig возвращает конфигурацию по умолчанию +func getDefaultConfig() ExampleConfig { + return ExampleConfig{ + ConfigPath: "../config.yaml", + SecretPath: "../secret.yaml", + ProjectPath: "../project.yaml", + LogLevel: "Info", + } +} + +// demonstrateBasicUsage демонстрирует базовое использование библиотеки +func demonstrateBasicUsage() error { + fmt.Println("=== Демонстрация базового использования библиотеки GitOps RAC ===") + + // Получаем конфигурацию + cfg := getDefaultConfig() + + // Создаем клиент + fmt.Println("Создание клиента...") + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: cfg.LogLevel, + }) + if err != nil { + return fmt.Errorf("ошибка создания клиента: %w", err) + } + fmt.Println("✓ Клиент успешно создан") + + // Создаем контекст с таймаутом + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Получаем текущий статус + fmt.Println("\nПроверка текущего статуса сервисного режима...") + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + return fmt.Errorf("ошибка получения статуса: %w", err) + } + fmt.Printf("✓ Текущий статус сервисного режима: %t\n", status) + + return nil +} + +// demonstrateServiceModeOperations демонстрирует операции с сервисным режимом +func demonstrateServiceModeOperations() error { + fmt.Println("\n=== Демонстрация операций с сервисным режимом ===") + + // Создаем клиент + cfg := getDefaultConfig() + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: "Debug", // Используем Debug для подробного логирования + }) + if err != nil { + return fmt.Errorf("ошибка создания клиента: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + // Получаем начальный статус + initialStatus, err := client.GetServiceModeStatus(ctx) + if err != nil { + return fmt.Errorf("ошибка получения начального статуса: %w", err) + } + fmt.Printf("Начальный статус: %t\n", initialStatus) + + // Если сервисный режим выключен, включаем его + if !initialStatus { + fmt.Println("\nВключение сервисного режима...") + if err := client.EnableServiceMode(ctx); err != nil { + return fmt.Errorf("ошибка включения сервисного режима: %w", err) + } + fmt.Println("✓ Сервисный режим включен") + + // Проверяем статус после включения + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + return fmt.Errorf("ошибка проверки статуса после включения: %w", err) + } + fmt.Printf("✓ Статус после включения: %t\n", status) + + // Ждем немного + time.Sleep(2 * time.Second) + + // Выключаем сервисный режим + fmt.Println("\nВыключение сервисного режима...") + if err := client.DisableServiceMode(ctx); err != nil { + return fmt.Errorf("ошибка выключения сервисного режима: %w", err) + } + fmt.Println("✓ Сервисный режим выключен") + } else { + fmt.Println("\nВыключение сервисного режима...") + if err := client.DisableServiceMode(ctx); err != nil { + return fmt.Errorf("ошибка выключения сервисного режима: %w", err) + } + fmt.Println("✓ Сервисный режим выключен") + + // Проверяем статус после выключения + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + return fmt.Errorf("ошибка проверки статуса после выключения: %w", err) + } + fmt.Printf("✓ Статус после выключения: %t\n", status) + + // Ждем немного + time.Sleep(2 * time.Second) + + // Включаем сервисный режим обратно + fmt.Println("\nВключение сервисного режима обратно...") + if err := client.EnableServiceMode(ctx); err != nil { + return fmt.Errorf("ошибка включения сервисного режима: %w", err) + } + fmt.Println("✓ Сервисный режим включен обратно") + } + + // Финальная проверка статуса + finalStatus, err := client.GetServiceModeStatus(ctx) + if err != nil { + return fmt.Errorf("ошибка получения финального статуса: %w", err) + } + fmt.Printf("\n✓ Финальный статус: %t\n", finalStatus) + + return nil +} + +// demonstrateErrorHandling демонстрирует обработку ошибок +func demonstrateErrorHandling() { + fmt.Println("\n=== Демонстрация обработки ошибок ===") + + // Попытка создать клиент с неверными путями + fmt.Println("Попытка создания клиента с неверными путями...") + _, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: "nonexistent-config.yaml", + SecretPath: "nonexistent-secret.yaml", + ProjectPath: "nonexistent-project.yaml", + LogLevel: "Info", + }) + if err != nil { + fmt.Printf("✓ Ожидаемая ошибка: %v\n", err) + } else { + fmt.Println("⚠ Неожиданно: ошибка не возникла") + } + + // Попытка создать клиент с неверным уровнем логирования + fmt.Println("\nПопытка создания клиента с неверным уровнем логирования...") + cfg := getDefaultConfig() + cfg.LogLevel = "InvalidLevel" + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: cfg.LogLevel, + }) + if err != nil { + fmt.Printf("✓ Ошибка при неверном уровне логирования: %v\n", err) + } else if client != nil { + fmt.Println("✓ Клиент создан, но может использовать уровень логирования по умолчанию") + } +} + +// printUsageInstructions выводит инструкции по использованию +func printUsageInstructions() { + fmt.Println("\n=== Инструкции по использованию библиотеки GitOps RAC ===") + fmt.Println("\n1. Импортируйте библиотеку:") + fmt.Println(` import benadisrac "git.benadis.ru/gitops/benadis-rac/"`) + + fmt.Println("\n2. Создайте конфигурацию:") + fmt.Println(` cfg := benadisrac.Config{`) + fmt.Println(` ConfigPath: "config.yaml",`) + fmt.Println(` SecretPath: "secret.yaml",`) + fmt.Println(` ProjectPath: "project.yaml",`) + fmt.Println(` LogLevel: "Info",`) + fmt.Println(` }`) + + fmt.Println("\n3. Создайте клиент:") + fmt.Println(` client, err := benadisrac.NewClient(cfg)`) + fmt.Println(` if err != nil {`) + fmt.Println(` log.Fatal(err)`) + fmt.Println(` }`) + + fmt.Println("\n4. Используйте методы клиента:") + fmt.Println(` ctx := context.Background()`) + fmt.Println(` status, err := client.GetServiceModeStatus(ctx)`) + fmt.Println(` err = client.EnableServiceMode(ctx)`) + fmt.Println(` err = client.DisableServiceMode(ctx)`) + + fmt.Println("\n5. Доступные уровни логирования: Debug, Info, Warn, Error") + fmt.Println("\n6. Все операции поддерживают context.Context для управления таймаутами") +} + +func main() { + fmt.Println("GitOps RAC Library Example") + fmt.Println("==========================") + + // Проверяем аргументы командной строки + if len(os.Args) > 1 { + switch os.Args[1] { + case "--help", "-h": + printUsageInstructions() + return + case "--basic": + if err := demonstrateBasicUsage(); err != nil { + log.Printf("Ошибка в базовой демонстрации: %v", err) + } + return + case "--operations": + if err := demonstrateServiceModeOperations(); err != nil { + log.Printf("Ошибка в демонстрации операций: %v", err) + } + return + case "--errors": + demonstrateErrorHandling() + return + } + } + + // Выполняем все демонстрации по умолчанию + fmt.Println("Запуск всех демонстраций...") + fmt.Println("Используйте флаги: --basic, --operations, --errors, --help") + fmt.Println() + + // Базовое использование + if err := demonstrateBasicUsage(); err != nil { + log.Printf("Ошибка в базовой демонстрации: %v", err) + } + + // Операции с сервисным режимом (только если базовая демонстрация прошла успешно) + if err := demonstrateServiceModeOperations(); err != nil { + log.Printf("Ошибка в демонстрации операций: %v", err) + } + + // Обработка ошибок + demonstrateErrorHandling() + + // Инструкции + printUsageInstructions() + + fmt.Println("\n=== Демонстрация завершена ===") +} diff --git a/example/main_test.go b/example/main_test.go new file mode 100644 index 0000000..9ad8b47 --- /dev/null +++ b/example/main_test.go @@ -0,0 +1,337 @@ +package main + +import ( + "context" + "os" + "testing" + "time" + + benadisrac "git.benadis.ru/gitops/benadis-rac" +) + +// TestGetDefaultConfig тестирует функцию получения конфигурации по умолчанию +func TestGetDefaultConfig(t *testing.T) { + cfg := getDefaultConfig() + + if cfg.ConfigPath == "" { + t.Error("ConfigPath не должен быть пустым") + } + if cfg.SecretPath == "" { + t.Error("SecretPath не должен быть пустым") + } + if cfg.ProjectPath == "" { + t.Error("ProjectPath не должен быть пустым") + } + if cfg.LogLevel == "" { + t.Error("LogLevel не должен быть пустым") + } + + // Проверяем ожидаемые значения + expected := ExampleConfig{ + ConfigPath: "../config.yaml", + SecretPath: "../secret.yaml", + ProjectPath: "../project.yaml", + LogLevel: "Info", + } + + if cfg != expected { + t.Errorf("Неожиданная конфигурация. Получено: %+v, ожидалось: %+v", cfg, expected) + } +} + +// TestClientCreation тестирует создание клиента с различными конфигурациями +func TestClientCreation(t *testing.T) { + tests := []struct { + name string + config benadisrac.Config + expectError bool + }{ + { + name: "Валидная конфигурация", + config: benadisrac.Config{ + ConfigPath: "../config.yaml", + SecretPath: "../secret.yaml", + ProjectPath: "../project.yaml", + LogLevel: "Info", + }, + expectError: false, + }, + { + name: "Несуществующий файл конфигурации", + config: benadisrac.Config{ + ConfigPath: "nonexistent.yaml", + SecretPath: "../secret.yaml", + ProjectPath: "../project.yaml", + LogLevel: "Info", + }, + expectError: true, + }, + { + name: "Несуществующий файл секретов", + config: benadisrac.Config{ + ConfigPath: "../config.yaml", + SecretPath: "nonexistent.yaml", + ProjectPath: "../project.yaml", + LogLevel: "Info", + }, + expectError: true, + }, + { + name: "Несуществующий файл проекта", + config: benadisrac.Config{ + ConfigPath: "../config.yaml", + SecretPath: "../secret.yaml", + ProjectPath: "nonexistent.yaml", + LogLevel: "Info", + }, + expectError: true, + }, + { + name: "Пустые пути (должны использоваться значения по умолчанию)", + config: benadisrac.Config{ + LogLevel: "Debug", + }, + expectError: true, // Файлы по умолчанию не существуют в example директории + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := benadisrac.NewClient(tt.config) + + if tt.expectError { + if err == nil { + t.Error("Ожидалась ошибка, но её не было") + } + if client != nil { + t.Error("Клиент не должен быть создан при ошибке") + } + } else { + if err != nil { + t.Errorf("Неожиданная ошибка: %v", err) + } + if client == nil { + t.Error("Клиент должен быть создан") + } + } + }) + } +} + +// TestClientOperations тестирует операции клиента (требует валидную конфигурацию) +func TestClientOperations(t *testing.T) { + // Проверяем наличие файлов конфигурации + cfg := getDefaultConfig() + if !fileExists(cfg.ConfigPath) || !fileExists(cfg.SecretPath) || !fileExists(cfg.ProjectPath) { + t.Skip("Пропускаем тест: файлы конфигурации не найдены") + } + + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: "Info", + }) + if err != nil { + t.Fatalf("Ошибка создания клиента: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Тестируем получение статуса + t.Run("GetServiceModeStatus", func(t *testing.T) { + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + t.Errorf("Ошибка получения статуса: %v", err) + } + // Статус может быть true или false, оба варианта валидны + t.Logf("Текущий статус сервисного режима: %t", status) + }) + + // Тестируем включение сервисного режима + t.Run("EnableServiceMode", func(t *testing.T) { + err := client.EnableServiceMode(ctx) + if err != nil { + t.Errorf("Ошибка включения сервисного режима: %v", err) + } + }) + + // Тестируем выключение сервисного режима + t.Run("DisableServiceMode", func(t *testing.T) { + err := client.DisableServiceMode(ctx) + if err != nil { + t.Errorf("Ошибка выключения сервисного режима: %v", err) + } + }) +} + +// TestClientWithContext тестирует работу с контекстом +func TestClientWithContext(t *testing.T) { + cfg := getDefaultConfig() + if !fileExists(cfg.ConfigPath) || !fileExists(cfg.SecretPath) || !fileExists(cfg.ProjectPath) { + t.Skip("Пропускаем тест: файлы конфигурации не найдены") + } + + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: "Info", + }) + if err != nil { + t.Fatalf("Ошибка создания клиента: %v", err) + } + + // Тестируем таймаут контекста + t.Run("ContextTimeout", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + // Ждем, чтобы контекст истек + time.Sleep(2 * time.Millisecond) + + _, err := client.GetServiceModeStatus(ctx) + if err == nil { + t.Log("Операция завершилась быстрее таймаута или таймаут не обрабатывается") + } else { + t.Logf("Ожидаемая ошибка таймаута: %v", err) + } + }) + + // Тестируем отмену контекста + t.Run("ContextCancellation", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Отменяем контекст сразу + + _, err := client.GetServiceModeStatus(ctx) + if err == nil { + t.Log("Операция завершилась до проверки отмены или отмена не обрабатывается") + } else { + t.Logf("Ожидаемая ошибка отмены: %v", err) + } + }) +} + +// TestDemonstrateBasicUsage тестирует функцию демонстрации базового использования +func TestDemonstrateBasicUsage(t *testing.T) { + cfg := getDefaultConfig() + if !fileExists(cfg.ConfigPath) || !fileExists(cfg.SecretPath) || !fileExists(cfg.ProjectPath) { + t.Skip("Пропускаем тест: файлы конфигурации не найдены") + } + + err := demonstrateBasicUsage() + if err != nil { + t.Errorf("Ошибка в демонстрации базового использования: %v", err) + } +} + +// TestDemonstrateErrorHandling тестирует функцию демонстрации обработки ошибок +func TestDemonstrateErrorHandling(t *testing.T) { + // Эта функция не должна вызывать панику + defer func() { + if r := recover(); r != nil { + t.Errorf("Функция demonstrateErrorHandling вызвала панику: %v", r) + } + }() + + demonstrateErrorHandling() +} + +// TestMainWithArguments тестирует обработку аргументов командной строки +func TestMainWithArguments(t *testing.T) { + tests := []struct { + name string + args []string + }{ + {"Help", []string{"program", "--help"}}, + {"Help short", []string{"program", "-h"}}, + {"Errors", []string{"program", "--errors"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Сохраняем оригинальные аргументы + origArgs := os.Args + defer func() { os.Args = origArgs }() + + // Устанавливаем тестовые аргументы + os.Args = tt.args + + // Проверяем, что функции не вызывают панику + defer func() { + if r := recover(); r != nil { + t.Errorf("Функция вызвала панику с аргументами %v: %v", tt.args, r) + } + }() + + // Тестируем отдельные функции вместо main() + switch tt.name { + case "Help", "Help short": + printUsageInstructions() + case "Errors": + demonstrateErrorHandling() + } + }) + } +} + +// Вспомогательные функции + +// fileExists проверяет существование файла +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return !os.IsNotExist(err) +} + +// BenchmarkClientCreation бенчмарк создания клиента +func BenchmarkClientCreation(b *testing.B) { + cfg := getDefaultConfig() + if !fileExists(cfg.ConfigPath) || !fileExists(cfg.SecretPath) || !fileExists(cfg.ProjectPath) { + b.Skip("Пропускаем бенчмарк: файлы конфигурации не найдены") + } + + config := benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: "Info", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + client, err := benadisrac.NewClient(config) + if err != nil { + b.Fatalf("Ошибка создания клиента: %v", err) + } + _ = client + } +} + +// BenchmarkGetStatus бенчмарк получения статуса +func BenchmarkGetStatus(b *testing.B) { + cfg := getDefaultConfig() + if !fileExists(cfg.ConfigPath) || !fileExists(cfg.SecretPath) || !fileExists(cfg.ProjectPath) { + b.Skip("Пропускаем бенчмарк: файлы конфигурации не найдены") + } + + client, err := benadisrac.NewClient(benadisrac.Config{ + ConfigPath: cfg.ConfigPath, + SecretPath: cfg.SecretPath, + ProjectPath: cfg.ProjectPath, + LogLevel: "Info", + }) + if err != nil { + b.Fatalf("Ошибка создания клиента: %v", err) + } + + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := client.GetServiceModeStatus(ctx) + if err != nil { + b.Fatalf("Ошибка получения статуса: %v", err) + } + } +} diff --git a/gitops_rac.go b/gitops_rac.go new file mode 100644 index 0000000..bbf9750 --- /dev/null +++ b/gitops_rac.go @@ -0,0 +1,124 @@ +// Package gitops_rac provides functionality for managing 1C service mode +package gitops_rac + +import ( + "context" + "fmt" + + "git.benadis.ru/gitops/benadis-rac/internal/config" + "git.benadis.ru/gitops/benadis-rac/internal/logger" + "git.benadis.ru/gitops/benadis-rac/internal/service" +) + +// ServiceModeManager интерфейс для управления сервисным режимом +type ServiceModeManager interface { + // EnableServiceMode включает сервисный режим + EnableServiceMode(ctx context.Context) error + + // DisableServiceMode выключает сервисный режим + DisableServiceMode(ctx context.Context) error + + // GetServiceModeStatus получает текущий статус сервисного режима + GetServiceModeStatus(ctx context.Context) (bool, error) +} + +// Client клиент для работы с 1C сервисным режимом +type Client struct { + service *service.ServiceModeService +} + +// Config конфигурация для создания клиента +type Config struct { + ConfigPath string // Путь к файлу конфигурации + SecretPath string // Путь к файлу с секретами + ProjectPath string // Путь к файлу проекта + LogLevel string // Уровень логирования (Debug, Info, Warn, Error) +} + +// NewClient создает новый клиент для управления сервисным режимом +func NewClient(cfg Config) (*Client, error) { + if cfg.ConfigPath == "" { + cfg.ConfigPath = "config.yaml" + } + if cfg.SecretPath == "" { + cfg.SecretPath = "secret.yaml" + } + if cfg.ProjectPath == "" { + cfg.ProjectPath = "project.yaml" + } + if cfg.LogLevel == "" { + cfg.LogLevel = "Info" + } + + // Загружаем конфигурацию + appConfig, err := config.LoadConfig(cfg.ConfigPath, cfg.SecretPath, cfg.ProjectPath) + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + + // Переопределяем уровень логирования если указан + if appConfig.Project != nil && appConfig.Project.ServiceMode != nil { + appConfig.Project.ServiceMode.LogLevel = cfg.LogLevel + } + + // Валидируем конфигурацию + if err := appConfig.Validate(); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) + } + + // Создаем логгер + logLevel := "Info" + if appConfig.Project != nil && appConfig.Project.ServiceMode != nil && appConfig.Project.ServiceMode.LogLevel != "" { + logLevel = appConfig.Project.ServiceMode.LogLevel + } + log := logger.NewLogger(logLevel) + + // Создаем сервис + svc := service.NewServiceModeService(appConfig, log) + + return &Client{ + service: svc, + }, nil +} + +// EnableServiceMode включает сервисный режим +func (c *Client) EnableServiceMode(ctx context.Context) error { + return c.service.EnableServiceMode(ctx) +} + +// DisableServiceMode выключает сервисный режим +func (c *Client) DisableServiceMode(ctx context.Context) error { + return c.service.DisableServiceMode(ctx) +} + +// GetServiceModeStatus получает текущий статус сервисного режима +// Возвращает true если сервисный режим включен, false если выключен +func (c *Client) GetServiceModeStatus(ctx context.Context) (bool, error) { + return c.service.GetServiceModeStatus(ctx) +} + +// NewClientFromConfig создает клиент из готовой конфигурации +func NewClientFromConfig(appConfig *config.AppConfig, logLevel string) (*Client, error) { + if logLevel != "" && appConfig.Project != nil && appConfig.Project.ServiceMode != nil { + appConfig.Project.ServiceMode.LogLevel = logLevel + } + + // Валидируем конфигурацию + if err := appConfig.Validate(); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) + } + + // Создаем логгер + logLevelToUse := "Info" + if appConfig.Project != nil && appConfig.Project.ServiceMode != nil && appConfig.Project.ServiceMode.LogLevel != "" { + logLevelToUse = appConfig.Project.ServiceMode.LogLevel + } + log := logger.NewLogger(logLevelToUse) + + // Создаем сервис + svc := service.NewServiceModeService(appConfig, log) + + return &Client{ + service: svc, + }, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7503bfe --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.benadis.ru/gitops/benadis-rac + +go 1.24.5 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..512fd3d --- /dev/null +++ b/integration_test.go @@ -0,0 +1,180 @@ +//go:build integration +// +build integration + +package gitops_rac + +import ( + "context" + "testing" + "time" +) + +// TestIntegrationServiceMode тестирует реальную работу с 1С сервером +// Для запуска используйте: go test -tags=integration +func TestIntegrationServiceMode(t *testing.T) { + // Создаем клиент с реальными конфигурационными файлами + client, err := NewClient(Config{ + ConfigPath: "config.yaml", + SecretPath: "secret.yaml", + LogLevel: "Debug", + }) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + // Получаем текущий статус + initialStatus, err := client.GetServiceModeStatus(ctx) + if err != nil { + t.Fatalf("Failed to get initial service mode status: %v", err) + } + t.Logf("Initial service mode status: %v", initialStatus) + + // Тестируем включение сервисного режима + t.Run("Enable Service Mode", func(t *testing.T) { + err := client.EnableServiceMode(ctx) + if err != nil { + t.Errorf("Failed to enable service mode: %v", err) + return + } + + // Проверяем что режим действительно включен + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + t.Errorf("Failed to get service mode status after enabling: %v", err) + return + } + + if !status { + t.Error("Service mode should be enabled but it's not") + } + t.Log("Service mode enabled successfully") + }) + + // Небольшая пауза между операциями + time.Sleep(2 * time.Second) + + // Тестируем выключение сервисного режима + t.Run("Disable Service Mode", func(t *testing.T) { + err := client.DisableServiceMode(ctx) + if err != nil { + t.Errorf("Failed to disable service mode: %v", err) + return + } + + // Проверяем что режим действительно выключен + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + t.Errorf("Failed to get service mode status after disabling: %v", err) + return + } + + if status { + t.Error("Service mode should be disabled but it's not") + } + t.Log("Service mode disabled successfully") + }) + + // Восстанавливаем исходное состояние + if initialStatus { + t.Log("Restoring initial service mode state (enabled)") + if err := client.EnableServiceMode(ctx); err != nil { + t.Errorf("Failed to restore initial service mode state: %v", err) + } + } else { + t.Log("Initial service mode state was disabled, no restoration needed") + } +} + +// TestIntegrationStatusOnly тестирует только получение статуса без изменений +func TestIntegrationStatusOnly(t *testing.T) { + client, err := NewClient(Config{ + ConfigPath: "config.yaml", + SecretPath: "secret.yaml", + LogLevel: "Info", + }) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + status, err := client.GetServiceModeStatus(ctx) + if err != nil { + t.Fatalf("Failed to get service mode status: %v", err) + } + + t.Logf("Current service mode status: %v", status) +} + +// TestIntegrationConfigValidation тестирует валидацию конфигурации +func TestIntegrationConfigValidation(t *testing.T) { + tests := []struct { + name string + configPath string + secretPath string + expectErr bool + }{ + { + name: "valid config files", + configPath: "config.yaml", + secretPath: "secret.yaml", + expectErr: false, + }, + { + name: "non-existent config file", + configPath: "non-existent-config.yaml", + secretPath: "secret.yaml", + expectErr: true, + }, + { + name: "non-existent secret file", + configPath: "config.yaml", + secretPath: "non-existent-secret.yaml", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewClient(Config{ + ConfigPath: tt.configPath, + SecretPath: tt.secretPath, + LogLevel: "Error", // Минимальный уровень логирования для тестов + }) + + if tt.expectErr && err == nil { + t.Error("Expected error but got none") + } + if !tt.expectErr && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + }) + } +} + +// BenchmarkServiceModeOperations бенчмарк для операций с сервисным режимом +func BenchmarkServiceModeOperations(b *testing.B) { + client, err := NewClient(Config{ + ConfigPath: "config.yaml", + SecretPath: "secret.yaml", + LogLevel: "Error", // Минимальное логирование для бенчмарка + }) + if err != nil { + b.Fatalf("Failed to create client: %v", err) + } + + ctx := context.Background() + + b.Run("GetStatus", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := client.GetServiceModeStatus(ctx) + if err != nil { + b.Errorf("Failed to get status: %v", err) + } + } + }) +} \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..209a9a2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,201 @@ +package config + +import ( + "fmt" + "os" + "time" + + "git.benadis.ru/gitops/benadis-rac/internal/constants" + + "gopkg.in/yaml.v3" +) + +// Config представляет основную конфигурацию приложения +type Config struct { + RACPath string `yaml:"rac_path"` + ConnectionTimeout string `yaml:"connection_timeout"` + CommandTimeout string `yaml:"command_timeout"` + RetryCount int `yaml:"retry_count"` + RetryDelay string `yaml:"retry_delay"` +} + +// GetConnectionTimeout возвращает ConnectionTimeout как time.Duration +func (c *Config) GetConnectionTimeout() (time.Duration, error) { + return time.ParseDuration(c.ConnectionTimeout) +} + +// GetCommandTimeout возвращает CommandTimeout как time.Duration +func (c *Config) GetCommandTimeout() (time.Duration, error) { + return time.ParseDuration(c.CommandTimeout) +} + +// GetRetryDelay возвращает RetryDelay как time.Duration +func (c *Config) GetRetryDelay() (time.Duration, error) { + return time.ParseDuration(c.RetryDelay) +} + +// ProjectCredentials представляет учетные данные для конкретного проекта +type ProjectCredentials struct { + ClusterAdmin string `yaml:"cluster_admin,omitempty"` + ClusterAdminPassword string `yaml:"cluster_admin_password,omitempty"` + DBAdmin string `yaml:"db_admin,omitempty"` + DBAdminPassword string `yaml:"db_admin_password,omitempty"` +} + +// Secret представляет секретную конфигурацию +type Secret struct { + ClusterAdmin string `yaml:"cluster_admin"` + ClusterAdminPassword string `yaml:"cluster_admin_password"` + DBAdmin string `yaml:"db_admin"` + DBAdminPassword string `yaml:"db_admin_password"` + Projects map[string]*ProjectCredentials `yaml:"projects,omitempty"` +} + +// ServiceModeConfig представляет конфигурацию service-mode из project.yaml +type ServiceModeConfig struct { + ServerHost string `yaml:"server_host"` + ServerPort int `yaml:"server_port"` + RACPort int `yaml:"rac_port"` + LogLevel string `yaml:"log_level"` + ServerName string `yaml:"server_name"` + BaseName string `yaml:"base_name"` + Command string `yaml:"command"` +} + +// ProjectConfig представляет конфигурацию проекта +type ProjectConfig struct { + ServiceMode *ServiceModeConfig `yaml:"service-mode"` +} + +// AppConfig объединяет все конфигурации +type AppConfig struct { + Config *Config + Secret *Secret + Project *ProjectConfig +} + +// LoadConfig загружает конфигурацию из файлов +func LoadConfig(configPath, secretPath, projectPath string) (*AppConfig, error) { + config, err := loadConfigFile(configPath) + if err != nil { + return nil, fmt.Errorf(constants.ErrLoadConfig, err) + } + + secret, err := loadSecretFile(secretPath) + if err != nil { + return nil, fmt.Errorf(constants.ErrLoadSecret, err) + } + + project, err := loadProjectFile(projectPath) + if err != nil { + return nil, fmt.Errorf("failed to load project config: %w", err) + } + + return &AppConfig{ + Config: config, + Secret: secret, + Project: project, + }, nil +} + +// loadConfigFile загружает основной конфигурационный файл +func loadConfigFile(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return &config, nil +} + +// loadSecretFile загружает файл с секретами +func loadSecretFile(path string) (*Secret, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read secret file: %w", err) + } + + var secret Secret + if err := yaml.Unmarshal(data, &secret); err != nil { + return nil, fmt.Errorf("failed to unmarshal secret: %w", err) + } + + return &secret, nil +} + +// loadProjectFile загружает файл с настройками проекта +func loadProjectFile(path string) (*ProjectConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read project file: %w", err) + } + + var project ProjectConfig + if err := yaml.Unmarshal(data, &project); err != nil { + return nil, fmt.Errorf("failed to unmarshal project: %w", err) + } + + return &project, nil +} + +// Validate проверяет корректность конфигурации +func (ac *AppConfig) Validate() error { + if ac.Config.RACPath == "" { + return fmt.Errorf(constants.ErrRACPathRequired) + } + if ac.Project == nil || ac.Project.ServiceMode == nil { + return fmt.Errorf("project service-mode configuration is required") + } + if ac.Project.ServiceMode.ServerHost == "" { + return fmt.Errorf(constants.ErrServerHostRequired) + } + if ac.Project.ServiceMode.BaseName == "" { + return fmt.Errorf(constants.ErrBaseNameRequired) + } + if ac.Secret.ClusterAdmin == "" { + return fmt.Errorf(constants.ErrClusterAdminRequired) + } + if ac.Secret.DBAdmin == "" { + return fmt.Errorf(constants.ErrDBAdminRequired) + } + + return nil +} + +// GetRACAddress возвращает адрес для подключения к RAC +func (ac *AppConfig) GetRACAddress() string { + return fmt.Sprintf("%s:%d", ac.Project.ServiceMode.ServerHost, ac.Project.ServiceMode.RACPort) +} + +// GetClusterCredentials возвращает учетные данные кластера с учетом приоритета проекта +func (ac *AppConfig) GetClusterCredentials(projectName string) (string, string) { + // Проверяем настройки для конкретного проекта + if ac.Secret.Projects != nil { + if projectCreds, exists := ac.Secret.Projects[projectName]; exists { + if projectCreds.ClusterAdmin != "" { + return projectCreds.ClusterAdmin, projectCreds.ClusterAdminPassword + } + } + } + // Возвращаем глобальные настройки + return ac.Secret.ClusterAdmin, ac.Secret.ClusterAdminPassword +} + +// GetDBCredentials возвращает учетные данные базы данных с учетом приоритета проекта +func (ac *AppConfig) GetDBCredentials(projectName string) (string, string) { + // Проверяем настройки для конкретного проекта + if ac.Secret.Projects != nil { + if projectCreds, exists := ac.Secret.Projects[projectName]; exists { + if projectCreds.DBAdmin != "" { + return projectCreds.DBAdmin, projectCreds.DBAdminPassword + } + } + } + // Возвращаем глобальные настройки + return ac.Secret.DBAdmin, ac.Secret.DBAdminPassword +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..d25f244 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,209 @@ +package config + +import ( + "os" + "testing" + "time" +) + +func TestLoadConfig(t *testing.T) { + // Создаем временные файлы для тестирования + configContent := `server_host: localhost +server_port: 1540 +rac_port: 1545 +rac_path: "/path/to/rac" +connection_timeout: 30s +command_timeout: 60s +retry_count: 3 +retry_delay: 5s +log_level: Debug +server_name: "Test Server"` + + secretContent := `cluster_admin: "admin" +cluster_admin_password: "password" +db_admin: "dbadmin" +db_admin_password: "dbpassword"` + + projectContent := `service-mode: + server_host: "localhost" + server_port: 1540 + rac_port: 1545 + base_name: "TestDB" + log_level: "Info"` + + // Создаем временные файлы + configFile, err := os.CreateTemp("", "config_*.yaml") + if err != nil { + t.Fatalf("Failed to create temp config file: %v", err) + } + defer os.Remove(configFile.Name()) + + secretFile, err := os.CreateTemp("", "secret_*.yaml") + if err != nil { + t.Fatalf("Failed to create temp secret file: %v", err) + } + defer os.Remove(secretFile.Name()) + + projectFile, err := os.CreateTemp("", "project_*.yaml") + if err != nil { + t.Fatalf("Failed to create temp project file: %v", err) + } + defer os.Remove(projectFile.Name()) + + // Записываем содержимое + if _, err := configFile.WriteString(configContent); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + configFile.Close() + + if _, err := secretFile.WriteString(secretContent); err != nil { + t.Fatalf("Failed to write secret file: %v", err) + } + secretFile.Close() + + if _, err := projectFile.WriteString(projectContent); err != nil { + t.Fatalf("Failed to write project file: %v", err) + } + projectFile.Close() + + // Тестируем загрузку конфигурации + appConfig, err := LoadConfig(configFile.Name(), secretFile.Name(), projectFile.Name()) + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) + } + + // Проверяем основную конфигурацию + if appConfig.Project.ServiceMode.ServerHost != "localhost" { + t.Errorf("Expected ServerHost 'localhost', got '%s'", appConfig.Project.ServiceMode.ServerHost) + } + + if appConfig.Project.ServiceMode.ServerPort != 1540 { + t.Errorf("Expected ServerPort 1540, got %d", appConfig.Project.ServiceMode.ServerPort) + } + + if appConfig.Project.ServiceMode.RACPort != 1545 { + t.Errorf("Expected RACPort 1545, got %d", appConfig.Project.ServiceMode.RACPort) + } + + connectionTimeout, err := appConfig.Config.GetConnectionTimeout() + if err != nil { + t.Errorf("Failed to parse ConnectionTimeout: %v", err) + } + if connectionTimeout != 30*time.Second { + t.Errorf("Expected ConnectionTimeout 30s, got %v", connectionTimeout) + } + + // Проверяем секретную конфигурацию + if appConfig.Secret.ClusterAdmin != "admin" { + t.Errorf("Expected ClusterAdmin 'admin', got '%s'", appConfig.Secret.ClusterAdmin) + } + + if appConfig.Secret.DBAdminPassword != "dbpassword" { + t.Errorf("Expected DBAdminPassword 'dbpassword', got '%s'", appConfig.Secret.DBAdminPassword) + } + + // Проверяем проектную конфигурацию + if appConfig.Project.ServiceMode.BaseName != "TestDB" { + t.Errorf("Expected BaseName 'TestDB', got '%s'", appConfig.Project.ServiceMode.BaseName) + } + + if appConfig.Project.ServiceMode.LogLevel != "Info" { + t.Errorf("Expected LogLevel 'Info', got '%s'", appConfig.Project.ServiceMode.LogLevel) + } +} + +func TestValidate(t *testing.T) { + tests := []struct { + name string + config *AppConfig + expectErr bool + }{ + { + name: "valid config", + config: &AppConfig{ + Config: &Config{ + RACPath: "/path/to/rac", + }, + Secret: &Secret{ + ClusterAdmin: "admin", + DBAdmin: "dbadmin", + }, + Project: &ProjectConfig{ + ServiceMode: &ServiceModeConfig{ + ServerHost: "localhost", + BaseName: "TestDB", + }, + }, + }, + expectErr: false, + }, + { + name: "missing server host", + config: &AppConfig{ + Config: &Config{ + RACPath: "/path/to/rac", + }, + Secret: &Secret{ + ClusterAdmin: "admin", + DBAdmin: "dbadmin", + }, + Project: &ProjectConfig{ + ServiceMode: &ServiceModeConfig{ + ServerHost: "", + BaseName: "TestDB", + }, + }, + }, + expectErr: true, + }, + { + name: "missing cluster admin", + config: &AppConfig{ + Config: &Config{ + RACPath: "/path/to/rac", + }, + Secret: &Secret{ + ClusterAdmin: "", + DBAdmin: "dbadmin", + }, + Project: &ProjectConfig{ + ServiceMode: &ServiceModeConfig{ + ServerHost: "localhost", + BaseName: "TestDB", + }, + }, + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.expectErr && err == nil { + t.Error("Expected error but got none") + } + if !tt.expectErr && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + }) + } +} + +func TestGetRACAddress(t *testing.T) { + config := &AppConfig{ + Project: &ProjectConfig{ + ServiceMode: &ServiceModeConfig{ + ServerHost: "example.com", + RACPort: 1545, + }, + }, + } + + expected := "example.com:1545" + actual := config.GetRACAddress() + + if actual != expected { + t.Errorf("Expected '%s', got '%s'", expected, actual) + } +} \ No newline at end of file diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 0000000..a6ec2f9 --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,291 @@ +package constants + +import "time" + +// Версия приложения +const ( + // AppVersion версия приложения + AppVersion = "1.0.0" +) + +// Константы для сервисного режима +const ( + // DefaultDeniedMessage сообщение по умолчанию при включении сервисного режима + DefaultDeniedMessage = "Техническое обслуживание. Попробуйте позже." + + // DefaultPermissionCode код разрешения для сервисного режима + DefaultPermissionCode = "service-mode" + + // TechnicalMaintenanceMessage сообщение для завершения сессий + TechnicalMaintenanceMessage = "Техническое обслуживание" +) + +// Константы таймаутов +const ( + // DEFAULT_CONNECTION_TIMEOUT таймаут подключения по умолчанию + DEFAULT_CONNECTION_TIMEOUT = 30 * time.Second + + // DEFAULT_COMMAND_TIMEOUT таймаут выполнения команды по умолчанию + DEFAULT_COMMAND_TIMEOUT = 60 * time.Second + + // DEFAULT_RETRY_COUNT количество попыток повтора по умолчанию + DEFAULT_RETRY_COUNT = 3 + + // DEFAULT_RETRY_DELAY задержка между попытками по умолчанию + DEFAULT_RETRY_DELAY = 5 * time.Second +) + +// Константы для RAC команд +const ( + // RAC_INFOBASE_UPDATE команда обновления информационной базы + RAC_INFOBASE_UPDATE = "infobase" + + // RAC_SESSION_LIST команда получения списка сессий + RAC_SESSION_LIST = "session" + + // RAC_CLUSTER_LIST команда получения списка кластеров + RAC_CLUSTER_LIST = "cluster" +) + +// Константы состояний сервисного режима +const ( + // SERVICE_MODE_ON включение сервисного режима + SERVICE_MODE_ON = "on" + + // SERVICE_MODE_OFF выключение сервисного режима + SERVICE_MODE_OFF = "off" +) + +// Константы для CLI параметров +const ( + // DefaultConfigPath путь к файлу конфигурации по умолчанию + DefaultConfigPath = "config.yaml" + + // DefaultSecretPath путь к файлу секретов по умолчанию + DefaultSecretPath = "secret.yaml" + + // CmdEnable команда включения сервисного режима + CmdEnable = "enable" + + // CmdDisable команда отключения сервисного режима + CmdDisable = "disable" + + // CmdStatus команда проверки статуса + CmdStatus = "status" +) + +// Константы для сообщений ошибок +const ( + // ErrCommandFailed сообщение об ошибке выполнения команды + ErrCommandFailed = "command failed after %d attempts: %w" + + // ErrFailedToParseCommandTimeout сообщение об ошибке парсинга таймаута команды + ErrFailedToParseCommandTimeout = "failed to parse command timeout: %w" + + // ErrRACCommandFailed сообщение об ошибке RAC команды + ErrRACCommandFailed = "RAC command failed: %w, output: %s" + + // ErrGetClusterList сообщение об ошибке получения списка кластеров + ErrGetClusterList = "failed to get cluster list: %w" + + // ErrClusterUUIDNotFound сообщение об ошибке поиска UUID кластера + ErrClusterUUIDNotFound = "cluster UUID not found in output" + + // ErrGetInfobaseList сообщение об ошибке получения списка информационных баз + ErrGetInfobaseList = "failed to get infobase list: %w" + + // ErrInfobaseUUIDNotFound сообщение об ошибке поиска UUID информационной базы + ErrInfobaseUUIDNotFound = "infobase UUID not found for name: %s" + + // ErrUnknownCommand сообщение об неизвестной команде + ErrUnknownCommand = "unknown command: %s" + + // ErrEnableServiceMode сообщение об ошибке включения сервисного режима + ErrEnableServiceMode = "failed to enable service mode: %w" + + // ErrDisableServiceMode сообщение об ошибке отключения сервисного режима + ErrDisableServiceMode = "failed to disable service mode: %w" + + // ErrGetSessions сообщение об ошибке получения списка сессий + ErrGetSessions = "failed to get sessions: %w" + + // ErrGetSessionList сообщение об ошибке получения списка сессий + ErrGetSessionList = "failed to get session list: %w" + + // ErrTerminateSession сообщение об ошибке завершения сессии + ErrTerminateSession = "failed to terminate session %s: %w" + + // ErrGetInfobaseInfo сообщение об ошибке получения информации о базе + ErrGetInfobaseInfo = "failed to get infobase info: %w" + + // ErrSessionsDenyVerification сообщение об ошибке проверки блокировки сессий + ErrSessionsDenyVerification = "sessions-deny verification failed: expected %s, got %s" + + // ErrScheduledJobsDenyVerification сообщение об ошибке проверки блокировки заданий + ErrScheduledJobsDenyVerification = "scheduled-jobs-deny verification failed: expected %s, got %s" + + // ErrLoadConfig сообщение об ошибке загрузки конфигурации + ErrLoadConfig = "failed to load config: %w" + + // ErrLoadSecret сообщение об ошибке загрузки секретов + ErrLoadSecret = "failed to load secret: %w" + + // ErrConfigValidation сообщение об ошибке валидации конфигурации + ErrConfigValidation = "config validation failed: %w" + + // ErrGetClusterUUID сообщение об ошибке получения UUID кластера + ErrGetClusterUUID = "failed to get cluster UUID: %w" + + // ErrGetInfobaseUUID сообщение об ошибке получения UUID информационной базы + ErrGetInfobaseUUID = "failed to get infobase UUID: %w" + + // ErrDetermineServiceModeStatus сообщение об ошибке определения статуса сервисного режима + ErrDetermineServiceModeStatus = "failed to determine service mode status: %w" + + // ErrReadConfigFile сообщение об ошибке чтения файла конфигурации + ErrReadConfigFile = "failed to read config file: %w" + + // ErrUnmarshalConfig сообщение об ошибке парсинга конфигурации + ErrUnmarshalConfig = "failed to unmarshal config: %w" + + // ErrReadSecretFile сообщение об ошибке чтения файла секретов + ErrReadSecretFile = "failed to read secret file: %w" + + // ErrUnmarshalSecret сообщение об ошибке парсинга секретов + ErrUnmarshalSecret = "failed to unmarshal secret: %w" + + // ErrServerHostRequired сообщение об обязательном поле server_host + ErrServerHostRequired = "server_host is required" + + // ErrRACPathRequired сообщение об обязательном поле rac_path + ErrRACPathRequired = "rac_path is required" + + // ErrBaseNameRequired сообщение об обязательном поле base_name + ErrBaseNameRequired = "base_name is required" + + // ErrClusterAdminRequired сообщение об обязательном поле cluster_admin + ErrClusterAdminRequired = "cluster_admin is required" + + // ErrDBAdminRequired сообщение об обязательном поле db_admin + ErrDBAdminRequired = "db_admin is required" +) + +// Константы для сообщений логгера +const ( + // LogMsgTerminatingAllSessions сообщение о завершении всех сессий + LogMsgTerminatingAllSessions = "Terminating all sessions" + + // LogMsgNoActiveSessions сообщение об отсутствии активных сессий + LogMsgNoActiveSessions = "No active sessions found" + + // LogMsgFoundActiveSessions сообщение о найденных активных сессиях + LogMsgFoundActiveSessions = "Found active sessions" + + // LogMsgSessionTerminated сообщение о завершении сессии + LogMsgSessionTerminated = "Session terminated" + + // LogMsgAllSessionsTerminated сообщение о завершении всех сессий + LogMsgAllSessionsTerminated = "All sessions termination completed" + + // LogMsgVerifyingServiceMode сообщение о проверке сервисного режима + LogMsgVerifyingServiceMode = "Verifying service mode" + + // LogMsgServiceModeEnabled сообщение об успешном включении сервисного режима + LogMsgServiceModeEnabled = "Service mode enabled successfully" + + // LogMsgServiceModeDisabled сообщение об успешном отключении сервисного режима + LogMsgServiceModeDisabled = "Service mode disabled successfully" + + // LogMsgServiceModeVerificationSuccessful сообщение об успешной проверке сервисного режима + LogMsgServiceModeVerificationSuccessful = "Service mode verification successful" + + // LogMsgExecutingRACCommand сообщение о выполнении RAC команды + LogMsgExecutingRACCommand = "Executing RAC command" + + // LogMsgRACCommandExecutedSuccessfully сообщение об успешном выполнении RAC команды + LogMsgRACCommandExecutedSuccessfully = "RAC command executed successfully" + + // LogMsgRACCommandFailed сообщение об ошибке выполнения RAC команды + LogMsgRACCommandFailed = "RAC command failed" + + // LogMsgFailedToTerminateSession сообщение об ошибке завершения сессии + LogMsgFailedToTerminateSession = "Failed to terminate session" + + // LogMsgCommandExecutionFailed сообщение об ошибке выполнения команды + LogMsgCommandExecutionFailed = "Command execution failed" + + // LogMsgRACCommand сообщение о RAC команде + LogMsgRACCommand = "RAC command" + + // LogMsgEnablingServiceMode сообщение о включении сервисного режима + LogMsgEnablingServiceMode = "Enabling service mode" + + // LogMsgDisablingServiceMode сообщение об отключении сервисного режима + LogMsgDisablingServiceMode = "Disabling service mode" + + // LogMsgFailedToTerminateAllSessions сообщение об ошибке завершения всех сессий + LogMsgFailedToTerminateAllSessions = "Failed to terminate all sessions" + + // LogMsgFailedToParseRetryDelay сообщение об ошибке парсинга задержки повтора + LogMsgFailedToParseRetryDelay = "Failed to parse retry delay, using default" + + // LogMsgRetryingAfterDelay сообщение о повторе после задержки + LogMsgRetryingAfterDelay = "Retrying after delay" + + // LogMsgRACCommandFailedAfterAllRetries сообщение об ошибке RAC команды после всех попыток + LogMsgRACCommandFailedAfterAllRetries = "RAC command failed after all retries" + + // LogMsgGettingClusterUUID сообщение о получении UUID кластера + LogMsgGettingClusterUUID = "Getting cluster UUID" + + // LogMsgFoundClusterUUID сообщение о найденном UUID кластера + LogMsgFoundClusterUUID = "Found cluster UUID" + + // LogMsgGettingInfobaseUUID сообщение о получении UUID информационной базы + LogMsgGettingInfobaseUUID = "Getting infobase UUID" + + // LogMsgFoundInfobaseUUID сообщение о найденном UUID информационной базы + LogMsgFoundInfobaseUUID = "Found infobase UUID" +) + +// Константы для статусных сообщений +const ( + // StatusServiceModeEnabled статус включенного сервисного режима + StatusServiceModeEnabled = "Service mode: ENABLED" + + // StatusServiceModeDisabled статус отключенного сервисного режима + StatusServiceModeDisabled = "Service mode: DISABLED" + + // MsgErrorLoadingConfig сообщение об ошибке загрузки конфигурации + MsgErrorLoadingConfig = "Error loading config: %v\n" + + // MsgConfigValidationError сообщение об ошибке валидации конфигурации + MsgConfigValidationError = "Config validation error: %v\n" + + // MsgVersionFormat формат вывода версии + MsgVersionFormat = "GitOps RAC v%s\n" +) + +// Константы для magic numbers +const ( + // UUIDLength длина UUID + UUIDLength = 36 + + // UUIDDashCount количество дефисов в UUID + UUIDDashCount = 4 + + // DefaultRACPort порт RAC по умолчанию + DefaultRACPort = 1545 + + // DefaultMainTimeout таймаут main функции + DefaultMainTimeout = 5 +) + +// Константы для логгера +const ( + // PasswordMask маска для паролей + PasswordMask = "***" + + // PasswordFlags флаги паролей через запятую для маскирования + PasswordFlags = "--cluster-pwd,--infobase-pwd" +) \ No newline at end of file diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..709b72b --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,110 @@ +package logger + +import ( + "log/slog" + "os" + "path" + "strings" + + "git.benadis.ru/gitops/benadis-rac/internal/constants" +) + +// Logger интерфейс для логирования +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Warn(msg string, args ...any) + Error(msg string, args ...any) + DebugCommand(msg string, command []string) +} + +// SlogLogger реализация Logger с использованием slog +type SlogLogger struct { + logger *slog.Logger +} + +// NewLogger создает новый логгер +func NewLogger(level string) Logger { + var logLevel slog.Level + switch strings.ToLower(level) { + case "debug": + logLevel = slog.LevelDebug + case "info": + logLevel = slog.LevelInfo + case "warn": + logLevel = slog.LevelWarn + case "error": + logLevel = slog.LevelError + default: + logLevel = slog.LevelInfo + } + + l := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: logLevel, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.SourceKey { + s := a.Value.Any().(*slog.Source) + s.File = path.Base(s.File) + } + return a + }, + })) + l = l.With(slog.Group("App info", + slog.String("version", constants.AppVersion), + )) + + return &SlogLogger{logger: l} +} + +// Debug логирует отладочное сообщение +func (l *SlogLogger) Debug(msg string, args ...any) { + l.logger.Debug(msg, args...) +} + +// Info логирует информационное сообщение +func (l *SlogLogger) Info(msg string, args ...any) { + l.logger.Info(msg, args...) +} + +// Warn логирует предупреждение +func (l *SlogLogger) Warn(msg string, args ...any) { + l.logger.Warn(msg, args...) +} + +// Error логирует ошибку +func (l *SlogLogger) Error(msg string, args ...any) { + l.logger.Error(msg, args...) +} + +// DebugCommand логирует команду с маскированием паролей +func (l *SlogLogger) DebugCommand(msg string, command []string) { + maskedCommand := maskPasswords(command) + l.logger.Debug(msg, "command", strings.Join(maskedCommand, " ")) +} + +// maskPasswords маскирует пароли в команде +func maskPasswords(command []string) []string { + masked := make([]string, len(command)) + copy(masked, command) + + // Преобразуем строку флагов в массив + passwordFlags := strings.Split(constants.PasswordFlags, ",") + + for i, arg := range masked { + for _, flag := range passwordFlags { + if strings.HasPrefix(arg, flag+"=") { + // Формат: --flag=value + parts := strings.SplitN(arg, "=", 2) + if len(parts) == 2 { + masked[i] = parts[0] + "=" + constants.PasswordMask + } + } else if arg == flag && i+1 < len(masked) { + // Формат: --flag value + masked[i+1] = constants.PasswordMask + } + } + } + + return masked +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 0000000..4629e1d --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,238 @@ +package logger + +import ( + "testing" +) + +func TestMaskPasswords(t *testing.T) { + tests := []struct { + name string + command []string + expected []string + }{ + { + name: "mask cluster password with equals", + command: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd=secret123", + "--other-flag=value", + }, + expected: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd=***", + "--other-flag=value", + }, + }, + { + name: "mask infobase password with equals", + command: []string{ + "rac", "localhost:1545", "infobase", "update", + "--infobase-pwd=secret456", + }, + expected: []string{ + "rac", "localhost:1545", "infobase", "update", + "--infobase-pwd=***", + }, + }, + { + name: "mask password with space separator", + command: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd", "secret789", + "--other-flag", "value", + }, + expected: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd", "***", + "--other-flag", "value", + }, + }, + { + name: "mask both passwords", + command: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd=cluster_secret", + "--infobase-pwd=infobase_secret", + }, + expected: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd=***", + "--infobase-pwd=***", + }, + }, + { + name: "no passwords to mask", + command: []string{ + "rac", "localhost:1545", "cluster", "list", + "--some-flag=value", + }, + expected: []string{ + "rac", "localhost:1545", "cluster", "list", + "--some-flag=value", + }, + }, + { + name: "mixed format passwords", + command: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd=secret1", + "--infobase-pwd", "secret2", + }, + expected: []string{ + "rac", "localhost:1545", "infobase", "update", + "--cluster-pwd=***", + "--infobase-pwd", "***", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := maskPasswords(tt.command) + + if len(actual) != len(tt.expected) { + t.Errorf("Expected length %d, got %d", len(tt.expected), len(actual)) + return + } + + for i, expected := range tt.expected { + if actual[i] != expected { + t.Errorf("At index %d: expected '%s', got '%s'", i, expected, actual[i]) + } + } + }) + } +} + +func TestNewLogger(t *testing.T) { + tests := []struct { + name string + level string + expected string // Мы не можем напрямую проверить уровень, но можем проверить что логгер создается + }{ + {"debug level", "debug", "debug"}, + {"info level", "info", "info"}, + {"warn level", "warn", "warn"}, + {"error level", "error", "error"}, + {"unknown level defaults to info", "unknown", "info"}, + {"empty level defaults to info", "", "info"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := NewLogger(tt.level) + if logger == nil { + t.Error("Expected logger to be created, got nil") + } + + // Проверяем что логгер реализует интерфейс Logger + var _ Logger = logger + }) + } +} + +func TestSlogLoggerMethods(t *testing.T) { + // Создаем логгер для тестирования + logger := NewLogger("debug") + + // Тестируем что методы не паникуют + t.Run("debug method", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Debug method panicked: %v", r) + } + }() + logger.Debug("test debug message", "key", "value") + }) + + t.Run("info method", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Info method panicked: %v", r) + } + }() + logger.Info("test info message", "key", "value") + }) + + t.Run("warn method", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Warn method panicked: %v", r) + } + }() + logger.Warn("test warn message", "key", "value") + }) + + t.Run("error method", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Error method panicked: %v", r) + } + }() + logger.Error("test error message", "key", "value") + }) + + t.Run("debug command method", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("DebugCommand method panicked: %v", r) + } + }() + command := []string{"rac", "--cluster-pwd=secret", "command"} + logger.DebugCommand("test command", command) + }) +} + +func TestMaskPasswordsEdgeCases(t *testing.T) { + tests := []struct { + name string + command []string + expected []string + }{ + { + name: "empty command", + command: []string{}, + expected: []string{}, + }, + { + name: "single element", + command: []string{"rac"}, + expected: []string{"rac"}, + }, + { + name: "password flag at end without value", + command: []string{ + "rac", "localhost:1545", "--cluster-pwd", + }, + expected: []string{ + "rac", "localhost:1545", "--cluster-pwd", + }, + }, + { + name: "password flag with empty value", + command: []string{ + "rac", "localhost:1545", "--cluster-pwd=", + }, + expected: []string{ + "rac", "localhost:1545", "--cluster-pwd=***", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := maskPasswords(tt.command) + + if len(actual) != len(tt.expected) { + t.Errorf("Expected length %d, got %d", len(tt.expected), len(actual)) + return + } + + for i, expected := range tt.expected { + if actual[i] != expected { + t.Errorf("At index %d: expected '%s', got '%s'", i, expected, actual[i]) + } + } + }) + } +} \ No newline at end of file diff --git a/internal/rac/rac.go b/internal/rac/rac.go new file mode 100644 index 0000000..c4aaff5 --- /dev/null +++ b/internal/rac/rac.go @@ -0,0 +1,333 @@ +package rac + +import ( + "context" + "fmt" + "os/exec" + "strings" + "time" + "unicode/utf8" + + "git.benadis.ru/gitops/benadis-rac/internal/config" + "git.benadis.ru/gitops/benadis-rac/internal/constants" + "git.benadis.ru/gitops/benadis-rac/internal/logger" +) + +// Client представляет клиент для работы с RAC +type Client struct { + config *config.AppConfig + logger logger.Logger +} + +// NewClient создает новый RAC клиент +func NewClient(cfg *config.AppConfig, log logger.Logger) *Client { + return &Client{ + config: cfg, + logger: log, + } +} + +// ServiceModeParams параметры для управления сервисным режимом +type ServiceModeParams struct { + ClusterUUID string + InfobaseUUID string + Enable bool + DeniedMessage string + PermissionCode string +} + +// ExecuteCommand выполняет RAC команду с retry логикой +func (c *Client) ExecuteCommand(ctx context.Context, args []string) (string, error) { + var lastErr error + retryCount := c.config.Config.RetryCount + if retryCount == 0 { + retryCount = constants.DEFAULT_RETRY_COUNT + } + + retryDelay, err := c.config.Config.GetRetryDelay() + if err != nil { + c.logger.Warn(constants.LogMsgFailedToParseRetryDelay, "error", err) + retryDelay = constants.DEFAULT_RETRY_DELAY + } + + for attempt := 1; attempt <= retryCount; attempt++ { + c.logger.Debug(constants.LogMsgExecutingRACCommand, "attempt", attempt, "max_attempts", retryCount) + c.logger.DebugCommand(constants.LogMsgRACCommand, append([]string{c.config.Config.RACPath}, args...)) + + output, err := c.executeCommandOnce(ctx, args) + if err == nil { + c.logger.Debug(constants.LogMsgRACCommandExecutedSuccessfully, "output_length", len(output)) + return output, nil + } + + lastErr = err + c.logger.Warn(constants.LogMsgRACCommandFailed, "attempt", attempt, "error", err) + + if attempt < retryCount { + c.logger.Debug(constants.LogMsgRetryingAfterDelay, "delay", retryDelay) + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(retryDelay): + // Продолжаем + } + } + } + + c.logger.Error(constants.LogMsgRACCommandFailedAfterAllRetries, "error", lastErr) + return "", fmt.Errorf(constants.ErrCommandFailed, retryCount, lastErr) +} + +// executeCommandOnce выполняет RAC команду один раз +func (c *Client) executeCommandOnce(ctx context.Context, args []string) (string, error) { + cmdTimeout, err := c.config.Config.GetCommandTimeout() + if err != nil { + return "", fmt.Errorf(constants.ErrFailedToParseCommandTimeout, err) + } + ctx, cancel := context.WithTimeout(ctx, cmdTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, c.config.Config.RACPath, args...) + output, err := cmd.CombinedOutput() + + if err != nil { + // Преобразуем вывод в корректную кодировку + outputStr := convertToUTF8(output) + return "", fmt.Errorf(constants.ErrRACCommandFailed, err, outputStr) + } + + return convertToUTF8(output), nil +} + +// GetClusterUUID получает UUID кластера по имени сервера +func (c *Client) GetClusterUUID(ctx context.Context) (string, error) { + c.logger.Info(constants.LogMsgGettingClusterUUID) + + args := []string{ + c.config.GetRACAddress(), + "cluster", "list", + } + + output, err := c.ExecuteCommand(ctx, args) + if err != nil { + return "", fmt.Errorf(constants.ErrGetClusterList, err) + } + + // Парсим вывод для получения UUID кластера + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "cluster") { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + uuid := strings.TrimSpace(parts[1]) + c.logger.Debug(constants.LogMsgFoundClusterUUID, "uuid", uuid) + return uuid, nil + } + } + } + + return "", fmt.Errorf(constants.ErrClusterUUIDNotFound) +} + +// GetInfobaseUUID получает UUID информационной базы по имени +func (c *Client) GetInfobaseUUID(ctx context.Context, clusterUUID string) (string, error) { + c.logger.Info(constants.LogMsgGettingInfobaseUUID, "cluster_uuid", clusterUUID) + + // Получаем учетные данные кластера (пока используем пустое имя проекта для глобальных настроек) + clusterUser, clusterPwd := c.config.GetClusterCredentials("") + + args := []string{ + c.config.GetRACAddress(), + "infobase", "summary", "list", + "--cluster=" + clusterUUID, + "--cluster-user=" + clusterUser, + "--cluster-pwd=" + clusterPwd, + } + + output, err := c.ExecuteCommand(ctx, args) + if err != nil { + return "", fmt.Errorf(constants.ErrGetInfobaseList, err) + } + + // Парсим вывод для поиска информационной базы по имени + lines := strings.Split(output, "\n") + var currentInfobaseUUID string + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "infobase") { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + currentInfobaseUUID = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "name") && currentInfobaseUUID != "" { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + name := strings.TrimSpace(parts[1]) + if name == c.config.Project.ServiceMode.BaseName { + c.logger.Debug(constants.LogMsgFoundInfobaseUUID, "uuid", currentInfobaseUUID, "name", name) + return currentInfobaseUUID, nil + } + } + } + } + + return "", fmt.Errorf(constants.ErrInfobaseUUIDNotFound, c.config.Project.ServiceMode.BaseName) +} + +// convertToUTF8 преобразует байты из CP866 (DOS) или Windows-1251 в UTF-8 +func convertToUTF8(data []byte) string { + // Проверяем, является ли строка уже валидным UTF-8 + if utf8.Valid(data) { + return string(data) + } + + // Преобразование из CP866 (DOS) в UTF-8 + // Это основная кодировка для сообщений об ошибках RAC + runes := make([]rune, 0, len(data)) + for _, b := range data { + if b < 128 { + // ASCII символы остаются без изменений + runes = append(runes, rune(b)) + } else { + // Преобразование символов CP866 в Unicode + switch b { + // Кириллические символы CP866 + case 0x80: + runes = append(runes, 'А') // А + case 0x81: + runes = append(runes, 'Б') // Б + case 0x82: + runes = append(runes, 'В') // В + case 0x83: + runes = append(runes, 'Г') // Г + case 0x84: + runes = append(runes, 'Д') // Д + case 0x85: + runes = append(runes, 'Е') // Е + case 0x86: + runes = append(runes, 'Ж') // Ж + case 0x87: + runes = append(runes, 'З') // З + case 0x88: + runes = append(runes, 'И') // И + case 0x89: + runes = append(runes, 'Й') // Й + case 0x8A: + runes = append(runes, 'К') // К + case 0x8B: + runes = append(runes, 'Л') // Л + case 0x8C: + runes = append(runes, 'М') // М + case 0x8D: + runes = append(runes, 'Н') // Н + case 0x8E: + runes = append(runes, 'О') // О + case 0x8F: + runes = append(runes, 'П') // П + case 0x90: + runes = append(runes, 'Р') // Р + case 0x91: + runes = append(runes, 'С') // С + case 0x92: + runes = append(runes, 'Т') // Т + case 0x93: + runes = append(runes, 'У') // У + case 0x94: + runes = append(runes, 'Ф') // Ф + case 0x95: + runes = append(runes, 'Х') // Х + case 0x96: + runes = append(runes, 'Ц') // Ц + case 0x97: + runes = append(runes, 'Ч') // Ч + case 0x98: + runes = append(runes, 'Ш') // Ш + case 0x99: + runes = append(runes, 'Щ') // Щ + case 0x9A: + runes = append(runes, 'Ъ') // Ъ + case 0x9B: + runes = append(runes, 'Ы') // Ы + case 0x9C: + runes = append(runes, 'Ь') // Ь + case 0x9D: + runes = append(runes, 'Э') // Э + case 0x9E: + runes = append(runes, 'Ю') // Ю + case 0x9F: + runes = append(runes, 'Я') // Я + case 0xA0: + runes = append(runes, 'а') // а + case 0xA1: + runes = append(runes, 'б') // б + case 0xA2: + runes = append(runes, 'в') // в + case 0xA3: + runes = append(runes, 'г') // г + case 0xA4: + runes = append(runes, 'д') // д + case 0xA5: + runes = append(runes, 'е') // е + case 0xA6: + runes = append(runes, 'ж') // ж + case 0xA7: + runes = append(runes, 'з') // з + case 0xA8: + runes = append(runes, 'и') // и + case 0xA9: + runes = append(runes, 'й') // й + case 0xAA: + runes = append(runes, 'к') // к + case 0xAB: + runes = append(runes, 'л') // л + case 0xAC: + runes = append(runes, 'м') // м + case 0xAD: + runes = append(runes, 'н') // н + case 0xAE: + runes = append(runes, 'о') // о + case 0xAF: + runes = append(runes, 'п') // п + case 0xE0: + runes = append(runes, 'р') // р + case 0xE1: + runes = append(runes, 'с') // с + case 0xE2: + runes = append(runes, 'т') // т + case 0xE3: + runes = append(runes, 'у') // у + case 0xE4: + runes = append(runes, 'ф') // ф + case 0xE5: + runes = append(runes, 'х') // х + case 0xE6: + runes = append(runes, 'ц') // ц + case 0xE7: + runes = append(runes, 'ч') // ч + case 0xE8: + runes = append(runes, 'ш') // ш + case 0xE9: + runes = append(runes, 'щ') // щ + case 0xEA: + runes = append(runes, 'ъ') // ъ + case 0xEB: + runes = append(runes, 'ы') // ы + case 0xEC: + runes = append(runes, 'ь') // ь + case 0xED: + runes = append(runes, 'э') // э + case 0xEE: + runes = append(runes, 'ю') // ю + case 0xEF: + runes = append(runes, 'я') // я + default: + // Для неизвестных символов оставляем как есть + runes = append(runes, rune(b)) + } + } + } + + return string(runes) +} diff --git a/internal/rac/service_mode.go b/internal/rac/service_mode.go new file mode 100644 index 0000000..24ef0e2 --- /dev/null +++ b/internal/rac/service_mode.go @@ -0,0 +1,235 @@ +package rac + +import ( + "context" + "fmt" + "strings" + + "git.benadis.ru/gitops/benadis-rac/internal/constants" +) + +// EnableServiceMode включает сервисный режим +func (c *Client) EnableServiceMode(ctx context.Context, params ServiceModeParams) error { + c.logger.Info(constants.LogMsgEnablingServiceMode, "cluster_uuid", params.ClusterUUID, "infobase_uuid", params.InfobaseUUID) + + // Сначала отключаем всех пользователей + if err := c.TerminateAllSessions(ctx, params.ClusterUUID, params.InfobaseUUID); err != nil { + c.logger.Warn(constants.LogMsgFailedToTerminateAllSessions, "error", err) + // Продолжаем выполнение, так как это не критичная ошибка + } + + // Получаем учетные данные + clusterUser, clusterPwd := c.config.GetClusterCredentials("") + dbUser, dbPwd := c.config.GetDBCredentials("") + + // Включаем сервисный режим + args := []string{ + c.config.GetRACAddress(), + "infobase", "update", + "--cluster=" + params.ClusterUUID, + "--infobase=" + params.InfobaseUUID, + "--sessions-deny=" + constants.SERVICE_MODE_ON, + "--scheduled-jobs-deny=" + constants.SERVICE_MODE_ON, + "--denied-message=" + params.DeniedMessage, + "--permission-code=" + params.PermissionCode, + "--cluster-user=" + clusterUser, + "--cluster-pwd=" + clusterPwd, + "--infobase-user=" + dbUser, + "--infobase-pwd=" + dbPwd, + } + + _, err := c.ExecuteCommand(ctx, args) + if err != nil { + return fmt.Errorf(constants.ErrEnableServiceMode, err) + } + + c.logger.Info(constants.LogMsgServiceModeEnabled) + + // Верифицируем результат + return c.VerifyServiceMode(ctx, params.ClusterUUID, params.InfobaseUUID, true) +} + +// DisableServiceMode выключает сервисный режим +func (c *Client) DisableServiceMode(ctx context.Context, params ServiceModeParams) error { + c.logger.Info(constants.LogMsgDisablingServiceMode, "cluster_uuid", params.ClusterUUID, "infobase_uuid", params.InfobaseUUID) + + // Получаем учетные данные + clusterUser, clusterPwd := c.config.GetClusterCredentials("") + dbUser, dbPwd := c.config.GetDBCredentials("") + + args := []string{ + c.config.GetRACAddress(), + "infobase", "update", + "--cluster=" + params.ClusterUUID, + "--infobase=" + params.InfobaseUUID, + "--sessions-deny=" + constants.SERVICE_MODE_OFF, + "--scheduled-jobs-deny=" + constants.SERVICE_MODE_OFF, + "--cluster-user=" + clusterUser, + "--cluster-pwd=" + clusterPwd, + "--infobase-user=" + dbUser, + "--infobase-pwd=" + dbPwd, + } + + _, err := c.ExecuteCommand(ctx, args) + if err != nil { + return fmt.Errorf(constants.ErrDisableServiceMode, err) + } + + c.logger.Info(constants.LogMsgServiceModeDisabled) + + // Верифицируем результат + return c.VerifyServiceMode(ctx, params.ClusterUUID, params.InfobaseUUID, false) +} + +// TerminateAllSessions принудительно завершает все сессии пользователей +func (c *Client) TerminateAllSessions(ctx context.Context, clusterUUID, infobaseUUID string) error { + c.logger.Info(constants.LogMsgTerminatingAllSessions, "cluster_uuid", clusterUUID, "infobase_uuid", infobaseUUID) + + // Получаем список сессий + sessions, err := c.GetSessions(ctx, clusterUUID, infobaseUUID) + if err != nil { + return fmt.Errorf(constants.ErrGetSessions, err) + } + + if len(sessions) == 0 { + c.logger.Info(constants.LogMsgNoActiveSessions) + return nil + } + + c.logger.Info(constants.LogMsgFoundActiveSessions, "count", len(sessions)) + + // Завершаем каждую сессию + for _, sessionUUID := range sessions { + if err := c.TerminateSession(ctx, clusterUUID, sessionUUID); err != nil { + c.logger.Warn(constants.LogMsgFailedToTerminateSession, "session_uuid", sessionUUID, "error", err) + // Продолжаем с другими сессиями + } else { + c.logger.Debug(constants.LogMsgSessionTerminated, "session_uuid", sessionUUID) + } + } + + c.logger.Info(constants.LogMsgAllSessionsTerminated) + return nil +} + +// GetSessions получает список активных сессий +func (c *Client) GetSessions(ctx context.Context, clusterUUID, infobaseUUID string) ([]string, error) { + // Получаем учетные данные кластера + clusterUser, clusterPwd := c.config.GetClusterCredentials("") + + args := []string{ + c.config.GetRACAddress(), + "session", "list", + "--cluster=" + clusterUUID, + "--infobase=" + infobaseUUID, + "--cluster-user=" + clusterUser, + "--cluster-pwd=" + clusterPwd, + } + + output, err := c.ExecuteCommand(ctx, args) + if err != nil { + return nil, fmt.Errorf(constants.ErrGetSessionList, err) + } + + var sessions []string + lines := strings.Split(output, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + // Ищем строки вида "session : " + if strings.HasPrefix(line, "session") && strings.Contains(line, ":") { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + sessionUUID := strings.TrimSpace(parts[1]) + // Проверяем, что это действительно UUID (36 символов с дефисами) + if len(sessionUUID) == constants.UUIDLength && strings.Count(sessionUUID, "-") == constants.UUIDDashCount { + sessions = append(sessions, sessionUUID) + } + } + } + } + + return sessions, nil +} + +// TerminateSession завершает конкретную сессию +func (c *Client) TerminateSession(ctx context.Context, clusterUUID, sessionUUID string) error { + // Получаем учетные данные кластера + clusterUser, clusterPwd := c.config.GetClusterCredentials("") + + args := []string{ + c.config.GetRACAddress(), + "session", "terminate", + "--cluster=" + clusterUUID, + "--session=" + sessionUUID, + "--error-message=" + constants.TechnicalMaintenanceMessage, + "--cluster-user=" + clusterUser, + "--cluster-pwd=" + clusterPwd, + } + + _, err := c.ExecuteCommand(ctx, args) + if err != nil { + return fmt.Errorf(constants.ErrTerminateSession, sessionUUID, err) + } + + return nil +} + +// VerifyServiceMode проверяет состояние сервисного режима +func (c *Client) VerifyServiceMode(ctx context.Context, clusterUUID, infobaseUUID string, expectedEnabled bool) error { + c.logger.Info(constants.LogMsgVerifyingServiceMode, "cluster_uuid", clusterUUID, "infobase_uuid", infobaseUUID, "expected_enabled", expectedEnabled) + + // Получаем учетные данные + clusterUser, clusterPwd := c.config.GetClusterCredentials("") + dbUser, dbPwd := c.config.GetDBCredentials("") + + args := []string{ + c.config.GetRACAddress(), + "infobase", "info", + "--cluster=" + clusterUUID, + "--infobase=" + infobaseUUID, + "--cluster-user=" + clusterUser, + "--cluster-pwd=" + clusterPwd, + "--infobase-user=" + dbUser, + "--infobase-pwd=" + dbPwd, + } + + output, err := c.ExecuteCommand(ctx, args) + if err != nil { + return fmt.Errorf(constants.ErrGetInfobaseInfo, err) + } + + lines := strings.Split(output, "\n") + var sessionsDeny, scheduledJobsDeny string + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "sessions-deny") { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + sessionsDeny = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "scheduled-jobs-deny") { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + scheduledJobsDeny = strings.TrimSpace(parts[1]) + } + } + } + + expectedState := constants.SERVICE_MODE_OFF + if expectedEnabled { + expectedState = constants.SERVICE_MODE_ON + } + + if sessionsDeny != expectedState { + return fmt.Errorf(constants.ErrSessionsDenyVerification, expectedState, sessionsDeny) + } + + if scheduledJobsDeny != expectedState { + return fmt.Errorf(constants.ErrScheduledJobsDenyVerification, expectedState, scheduledJobsDeny) + } + + c.logger.Info(constants.LogMsgServiceModeVerificationSuccessful, "sessions_deny", sessionsDeny, "scheduled_jobs_deny", scheduledJobsDeny) + return nil +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..e8cb912 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,124 @@ +package service + +import ( + "context" + "fmt" + + "git.benadis.ru/gitops/benadis-rac/internal/config" + "git.benadis.ru/gitops/benadis-rac/internal/constants" + "git.benadis.ru/gitops/benadis-rac/internal/logger" + "git.benadis.ru/gitops/benadis-rac/internal/rac" +) + +// ServiceModeService сервис для управления сервисным режимом +type ServiceModeService struct { + racClient *rac.Client + config *config.AppConfig + logger logger.Logger +} + +// NewServiceModeService создает новый сервис +func NewServiceModeService(cfg *config.AppConfig, log logger.Logger) *ServiceModeService { + racClient := rac.NewClient(cfg, log) + return &ServiceModeService{ + racClient: racClient, + config: cfg, + logger: log, + } +} + +// EnableServiceMode включает сервисный режим +func (s *ServiceModeService) EnableServiceMode(ctx context.Context) error { + s.logger.Info("Starting service mode enablement") + + // Получаем UUID кластера + clusterUUID, err := s.racClient.GetClusterUUID(ctx) + if err != nil { + return fmt.Errorf(constants.ErrGetClusterUUID, err) + } + + // Получаем UUID информационной базы + infobaseUUID, err := s.racClient.GetInfobaseUUID(ctx, clusterUUID) + if err != nil { + return fmt.Errorf(constants.ErrGetInfobaseUUID, err) + } + + // Подготавливаем параметры + params := rac.ServiceModeParams{ + ClusterUUID: clusterUUID, + InfobaseUUID: infobaseUUID, + Enable: true, + DeniedMessage: constants.DefaultDeniedMessage, + PermissionCode: constants.DefaultPermissionCode, + } + + // Включаем сервисный режим + if err := s.racClient.EnableServiceMode(ctx, params); err != nil { + return fmt.Errorf(constants.ErrEnableServiceMode, err) + } + + s.logger.Info(constants.LogMsgServiceModeEnabled) + return nil +} + +// DisableServiceMode выключает сервисный режим +func (s *ServiceModeService) DisableServiceMode(ctx context.Context) error { + s.logger.Info("Starting service mode disablement") + + // Получаем UUID кластера + clusterUUID, err := s.racClient.GetClusterUUID(ctx) + if err != nil { + return fmt.Errorf(constants.ErrGetClusterUUID, err) + } + + // Получаем UUID информационной базы + infobaseUUID, err := s.racClient.GetInfobaseUUID(ctx, clusterUUID) + if err != nil { + return fmt.Errorf(constants.ErrGetInfobaseUUID, err) + } + + // Подготавливаем параметры + params := rac.ServiceModeParams{ + ClusterUUID: clusterUUID, + InfobaseUUID: infobaseUUID, + Enable: false, + } + + // Выключаем сервисный режим + if err := s.racClient.DisableServiceMode(ctx, params); err != nil { + return fmt.Errorf(constants.ErrDisableServiceMode, err) + } + + s.logger.Info(constants.LogMsgServiceModeDisabled) + return nil +} + +// GetServiceModeStatus получает текущий статус сервисного режима +func (s *ServiceModeService) GetServiceModeStatus(ctx context.Context) (bool, error) { + s.logger.Info("Getting service mode status") + + // Получаем UUID кластера + clusterUUID, err := s.racClient.GetClusterUUID(ctx) + if err != nil { + return false, fmt.Errorf(constants.ErrGetClusterUUID, err) + } + + // Получаем UUID информационной базы + infobaseUUID, err := s.racClient.GetInfobaseUUID(ctx, clusterUUID) + if err != nil { + return false, fmt.Errorf(constants.ErrGetInfobaseUUID, err) + } + + // Проверяем статус (используем VerifyServiceMode для проверки включенного состояния) + err = s.racClient.VerifyServiceMode(ctx, clusterUUID, infobaseUUID, true) + if err != nil { + // Если проверка на включенное состояние не прошла, проверяем выключенное + err = s.racClient.VerifyServiceMode(ctx, clusterUUID, infobaseUUID, false) + if err != nil { + return false, fmt.Errorf(constants.ErrDetermineServiceModeStatus, err) + } + return false, nil // Сервисный режим выключен + } + + return true, nil // Сервисный режим включен +} diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..9c8ab62 --- /dev/null +++ b/project.yaml @@ -0,0 +1,16 @@ +service-mode: + # Настройки подключения к кластеру + server_host: localhost + server_port: 1540 + rac_port: 1545 + + # Логирование + log_level: Debug + + # Параметры для команд + server_name: "Локальный кластер" + # Не изменяй этот параметр при тестировании! + base_name: "V8_DEV_DSBEKETOV_APK_TOIR3" + + # Команда для выполнения + command: "" \ No newline at end of file diff --git a/requirements.md b/requirements.md new file mode 100644 index 0000000..a6dc250 --- /dev/null +++ b/requirements.md @@ -0,0 +1,319 @@ +# Requirements для проекта GitOps RAC + +## Обзор проекта + +GitOps RAC - это Go-библиотека и CLI-утилита для управления сервисным режимом информационных баз 1С:Предприятие через утилиту RAC (Remote Administration Client). Проект реализует автоматизацию операций включения/выключения сервисного режима с поддержкой принудительного отключения пользователей и верификации операций. + +## Функциональные требования + +### 1. Основная функциональность + +#### 1.1 Управление сервисным режимом +- **Включение сервисного режима** с автоматическим отключением всех активных пользователей +- **Выключение сервисного режима** с восстановлением доступа +- **Проверка статуса** текущего состояния сервисного режима +- **Верификация операций** - проверка корректности выполнения команд + +#### 1.2 Работа с пользовательскими сессиями +- Получение списка активных сессий пользователей +- Принудительное завершение всех пользовательских сессий +- Обработка ошибок при завершении отдельных сессий (продолжение работы) + +#### 1.3 Интеграция с RAC +- Выполнение команд через утилиту rac.exe +- Автоматическое получение UUID кластера и информационной базы +- Поддержка аутентификации на уровне кластера и базы данных + +### 2. Интерфейсы использования + +#### 2.1 CLI интерфейс +- Команды: `enable`, `disable`, `status` +- Флаги: `-config`, `-secret`, `-command`, `-version`, `-help` +- Переопределение параметров конфигурации через командную строку + +#### 2.2 Библиотечный API +- Публичный интерфейс `ServiceModeManager` +- Методы: `EnableServiceMode()`, `DisableServiceMode()`, `GetServiceModeStatus()` +- Поддержка контекста для отмены операций + +## Нефункциональные требования + +### 3. Производительность и надежность + +#### 3.1 Retry-логика +- Настраиваемое количество попыток (по умолчанию: 3) +- Настраиваемая задержка между попытками (по умолчанию: 5 секунд) +- Обработка временных сбоев сети и сервера + +#### 3.2 Таймауты +- Таймаут подключения (по умолчанию: 30 секунд) +- Таймаут выполнения команды (по умолчанию: 60 секунд) +- Общий таймаут операции (по умолчанию: 5 минут) + +#### 3.3 Контекст и отмена операций +- Поддержка `context.Context` для всех операций +- Возможность отмены длительных операций +- Корректная обработка отмены контекста + +### 4. Безопасность + +#### 4.1 Управление секретами +- Разделение конфигурации и секретов (config.yaml / secret.yaml) +- Маскирование паролей в логах (замена на `***`) +- Отсутствие секретов в основной конфигурации + +#### 4.2 Валидация +- Валидация обязательных параметров конфигурации +- Проверка корректности путей к файлам +- Валидация входных параметров + +### 5. Логирование и мониторинг + +#### 5.1 Структурированное логирование +- Использование `log/slog` для JSON-логирования +- Уровни логирования: Debug, Info, Warn, Error +- Добавление метаданных (версия приложения, источник) + +#### 5.2 Детализация логов +- Логирование всех RAC команд (с маскированием паролей) +- Отслеживание попыток retry +- Логирование результатов верификации +- Информация о завершении сессий пользователей + +### 6. Конфигурация + +#### 6.1 Файлы конфигурации +**config.yaml** должен содержать: +- Параметры подключения (server_host, server_port, rac_port) +- Путь к rac.exe (rac_path) +- Настройки таймаутов и retry +- Параметры логирования +- Имена сервера и базы данных + +**secret.yaml** должен содержать: +- Учетные данные кластера (cluster_admin, cluster_admin_password) +- Учетные данные базы данных (db_admin, db_admin_password) + +#### 6.2 Настраиваемые параметры +- Сообщение для пользователей при включении сервисного режима +- Код разрешения для сервисного режима +- Все таймауты и параметры retry +- Уровень логирования + +## Архитектурные требования + +### 7. Структура проекта + +#### 7.1 Модульная архитектура +``` +benadis-rac/ +├── cmd/ # CLI приложение +├── internal/ # Внутренние пакеты +│ ├── config/ # Управление конфигурацией +│ ├── constants/ # Константы приложения +│ ├── logger/ # Логирование +│ ├── rac/ # Взаимодействие с RAC +│ └── service/ # Бизнес-логика +├── gitops_rac.go # Публичный API +└── integration_test.go # Интеграционные тесты +``` + +#### 7.2 Принципы проектирования +- **SOLID принципы** - каждый компонент имеет единственную ответственность +- **Dependency Injection** - внедрение зависимостей через конструкторы +- **Интерфейсы** - использование интерфейсов для абстракции (Logger, ServiceModeManager) +- **Модульность** - четкое разделение ответственности между пакетами + +### 8. Управление константами + +#### 8.1 Централизованные константы +- Все строковые литералы вынесены в пакет constants +- Сообщения об ошибках +- Сообщения для логирования +- Параметры по умолчанию +- RAC команды и параметры + +#### 8.2 Типизированные константы +- Версия приложения +- Таймауты (time.Duration) +- Числовые значения (порты, счетчики) +- Флаги паролей для маскирования + +## Требования к тестированию + +### 9. Unit тесты +- Покрытие всех публичных функций +- Тестирование обработки ошибок +- Мок-тесты для внешних зависимостей +- Тестирование валидации конфигурации + +### 10. Интеграционные тесты +- Тесты с реальным 1С сервером +- Проверка полного цикла операций +- Тестирование retry-логики +- Верификация маскирования паролей + +## Предложения по улучшению + +### 11. Расширение функциональности + +#### 11.1 Мониторинг и метрики +- **Экспорт метрик** в формате Prometheus + - Количество успешных/неуспешных операций + - Время выполнения операций + - Количество активных сессий +- **Health check endpoint** для проверки доступности +- **Статистика использования** команд и операций + +#### 11.2 Уведомления +- **Webhook уведомления** о смене статуса сервисного режима +- **Email уведомления** администраторам +- **Slack/Teams интеграция** для команд разработки +- **Настраиваемые шаблоны** уведомлений + +#### 11.3 Планировщик задач +- **Cron-подобный планировщик** для автоматического включения/выключения +- **Календарные окна обслуживания** +- **Интеграция с внешними планировщиками** (Kubernetes CronJob, systemd timer) + +### 12. Операционные улучшения + +#### 12.1 Конфигурация +- **Поддержка переменных окружения** для всех параметров +- **Конфигурация через Kubernetes ConfigMap/Secret** +- **Горячая перезагрузка конфигурации** без перезапуска +- **Валидация конфигурации** с детальными сообщениями об ошибках + +#### 12.2 Безопасность +- **Интеграция с внешними системами управления секретами**: + - HashiCorp Vault + - Kubernetes Secrets + - Azure Key Vault + - AWS Secrets Manager +- **Ротация паролей** с автоматическим обновлением +- **Аудит операций** с записью в отдельный лог +- **RBAC** для разграничения доступа к операциям + +#### 12.3 Мониторинг и диагностика +- **Structured logging** с дополнительными полями: + - Correlation ID для трассировки операций + - User ID инициатора операции + - Временные метки с микросекундами +- **Distributed tracing** (OpenTelemetry/Jaeger) +- **Профилирование производительности** (pprof) + +### 13. Масштабируемость + +#### 13.1 Поддержка множественных баз +- **Конфигурация нескольких информационных баз** +- **Групповые операции** над множеством баз +- **Параллельное выполнение** операций +- **Приоритизация** операций по базам + +#### 13.2 Кластеризация +- **Поддержка нескольких кластеров 1С** +- **Load balancing** между кластерами +- **Failover** при недоступности основного кластера +- **Синхронизация состояния** между экземплярами + +#### 13.3 Производительность +- **Пулы соединений** к RAC +- **Кэширование** UUID кластеров и баз +- **Асинхронные операции** для неблокирующего выполнения +- **Batch операции** для массовых изменений + +### 14. Интеграции + +#### 14.1 CI/CD интеграция +- **GitHub Actions** для автоматизации развертывания +- **GitLab CI** пайплайны +- **Jenkins** плагины +- **Helm charts** для Kubernetes развертывания + +#### 14.2 Мониторинг систем +- **Grafana dashboards** для визуализации метрик +- **Alertmanager** правила для уведомлений +- **Nagios/Zabbix** плагины для мониторинга +- **DataDog/New Relic** интеграция + +#### 14.3 API расширения +- **REST API** для внешних систем +- **GraphQL** для гибких запросов +- **gRPC** для высокопроизводительных интеграций +- **WebSocket** для real-time уведомлений + +### 15. Пользовательский интерфейс + +#### 15.1 Web интерфейс +- **Веб-панель управления** для администраторов +- **Dashboard** с текущим статусом всех баз +- **История операций** с возможностью фильтрации +- **Планировщик задач** с графическим интерфейсом + +#### 15.2 Мобильное приложение +- **Мобильное приложение** для экстренного управления +- **Push уведомления** о критических событиях +- **Биометрическая аутентификация** + +### 16. Документация и обучение + +#### 16.1 Документация +- **OpenAPI/Swagger** спецификация для API +- **Интерактивная документация** с примерами +- **Руководство по миграции** между версиями +- **Best practices** для production использования + +#### 16.2 Инструменты разработки +- **Docker образы** для разработки и тестирования +- **Makefile** для автоматизации сборки +- **Pre-commit hooks** для проверки качества кода +- **Benchmark тесты** для оценки производительности + +## Технические ограничения + +### 17. Совместимость +- **Go версия**: 1.24+ +- **1С:Предприятие**: 8.3.x +- **Операционные системы**: Windows, Linux, macOS +- **Архитектуры**: amd64, arm64 + +### 18. Зависимости +- Минимальное количество внешних зависимостей +- Использование только стабильных и поддерживаемых библиотек +- Регулярное обновление зависимостей для безопасности + +### 19. Производительность +- **Время отклика**: < 5 секунд для стандартных операций +- **Потребление памяти**: < 100MB в runtime +- **CPU использование**: < 10% при нормальной нагрузке +- **Concurrent операции**: до 10 одновременных операций + +## Критерии приемки + +### 20. Функциональные критерии +- ✅ Успешное включение/выключение сервисного режима +- ✅ Корректная верификация операций +- ✅ Принудительное отключение пользователей +- ✅ Маскирование паролей в логах +- ✅ Retry-логика при сбоях + +### 21. Качественные критерии +- ✅ Покрытие unit тестами > 80% +- ✅ Успешное прохождение интеграционных тестов +- ✅ Отсутствие критических уязвимостей безопасности +- ✅ Соответствие Go coding standards +- ✅ Полная документация API + +### 22. Операционные критерии +- ✅ Стабильная работа в production окружении +- ✅ Корректная обработка всех типов ошибок +- ✅ Информативное логирование для диагностики +- ✅ Возможность мониторинга состояния приложения + +## Заключение + +Проект GitOps RAC представляет собой зрелое решение для автоматизации управления сервисным режимом 1С:Предприятие. Текущая реализация покрывает основные потребности и следует лучшим практикам разработки на Go. + +Предложенные улучшения позволят расширить функциональность для enterprise использования, улучшить операционные характеристики и интегрироваться с современными DevOps практиками. + +Приоритизация улучшений должна основываться на конкретных потребностях пользователей и доступных ресурсах для разработки. \ No newline at end of file