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