This commit is contained in:
2025-08-04 11:03:25 +03:00
commit b1bde827de
20 changed files with 3579 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -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

414
README.md Normal file
View File

@@ -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 в репозитории проекта.

120
cmd/main.go Normal file
View File

@@ -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 <command>
Commands:
enable - Enable service mode
disable - Disable service mode
status - Show current service mode status
Options:
-config <path> Path to config file (default: config.yaml)
-secret <path> Path to secret file (default: secret.yaml)
-project <path> 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)
}

8
config.yaml Normal file
View File

@@ -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

266
example/main.go Normal file
View File

@@ -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=== Демонстрация завершена ===")
}

337
example/main_test.go Normal file
View File

@@ -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)
}
}
}

124
gitops_rac.go Normal file
View File

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

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module git.benadis.ru/gitops/benadis-rac
go 1.24.5
require gopkg.in/yaml.v3 v3.0.1

4
go.sum Normal file
View File

@@ -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=

180
integration_test.go Normal file
View File

@@ -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)
}
}
})
}

201
internal/config/config.go Normal file
View File

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

View File

@@ -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)
}
}

View File

@@ -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"
)

110
internal/logger/logger.go Normal file
View File

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

View File

@@ -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])
}
}
})
}
}

333
internal/rac/rac.go Normal file
View File

@@ -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)
}

View File

@@ -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 : <uuid>"
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
}

124
internal/service/service.go Normal file
View File

@@ -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 // Сервисный режим включен
}

16
project.yaml Normal file
View File

@@ -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: ""

319
requirements.md Normal file
View File

@@ -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 практиками.
Приоритизация улучшений должна основываться на конкретных потребностях пользователей и доступных ресурсах для разработки.