Init
This commit is contained in:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal 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
414
README.md
Normal 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
120
cmd/main.go
Normal 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
8
config.yaml
Normal 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
266
example/main.go
Normal 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
337
example/main_test.go
Normal 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
124
gitops_rac.go
Normal 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
5
go.mod
Normal 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
4
go.sum
Normal 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
180
integration_test.go
Normal 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
201
internal/config/config.go
Normal 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
|
||||
}
|
||||
209
internal/config/config_test.go
Normal file
209
internal/config/config_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
291
internal/constants/constants.go
Normal file
291
internal/constants/constants.go
Normal 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
110
internal/logger/logger.go
Normal 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
|
||||
}
|
||||
238
internal/logger/logger_test.go
Normal file
238
internal/logger/logger_test.go
Normal 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
333
internal/rac/rac.go
Normal 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)
|
||||
}
|
||||
235
internal/rac/service_mode.go
Normal file
235
internal/rac/service_mode.go
Normal 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
124
internal/service/service.go
Normal 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
16
project.yaml
Normal 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
319
requirements.md
Normal 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 практиками.
|
||||
|
||||
Приоритизация улучшений должна основываться на конкретных потребностях пользователей и доступных ресурсах для разработки.
|
||||
Reference in New Issue
Block a user