feat: AI-анализ terraform plan (уровни 0-5, Google Gemini)
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// cmd/sless-plan/main.go
|
||||
// Дата: 2026-03-12
|
||||
// CLI-обёртка над `terraform plan`.
|
||||
// Добавляет AI-анализ вывода если AI_HINT_LEVEL > 0.
|
||||
// Существующий terraform plan не меняется — AI только дополняет вывод.
|
||||
//
|
||||
// Использование:
|
||||
// sless-plan [terraform args...]
|
||||
// sless-plan --ai-level=3 [terraform args...]
|
||||
//
|
||||
// Env-переменные:
|
||||
// AI_HINT_LEVEL — уровень анализа 0–5 (0 = выключен, по умолчанию)
|
||||
// AI_PROVIDER — провайдер: "google" (по умолчанию) или "cloud"
|
||||
// GOOGLE_AI_API_KEY — API ключ Google Gemini
|
||||
// GOOGLE_AI_MODEL — модель (по умолчанию "gemini-2.0-flash")
|
||||
// CLOUD_LLM_ENDPOINT — endpoint будущего облачного LLM
|
||||
// CLOUD_LLM_TOKEN — токен для облачного LLM
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/ai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Флаг --ai-level переопределяет env AI_HINT_LEVEL
|
||||
aiLevel := flag.Int("ai-level", -1, "уровень AI-анализа 0–5 (-1 = читать из AI_HINT_LEVEL)")
|
||||
flag.Parse()
|
||||
|
||||
// Определяем уровень: флаг > env > 0
|
||||
level := resolveLevel(*aiLevel)
|
||||
|
||||
// Собираем аргументы для terraform plan (всё что после флагов sless-plan)
|
||||
tfArgs := append([]string{"plan"}, flag.Args()...)
|
||||
|
||||
// Запускаем terraform plan, захватывая вывод для AI
|
||||
// но одновременно выводя его пользователю (tee-эффект)
|
||||
planOutput, exitCode := runTerraform(tfArgs)
|
||||
|
||||
// Если уровень > 0 — запускаем AI анализ
|
||||
if level > 0 {
|
||||
runAIAnalysis(level, planOutput)
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// resolveLevel определяет уровень AI анализа.
|
||||
// Приоритет: --ai-level flag > AI_HINT_LEVEL env > 0
|
||||
func resolveLevel(flagValue int) int {
|
||||
if flagValue >= 0 {
|
||||
// Флаг явно задан
|
||||
if flagValue > 5 {
|
||||
flagValue = 5
|
||||
}
|
||||
return flagValue
|
||||
}
|
||||
|
||||
// Читаем из env
|
||||
if envVal := os.Getenv("AI_HINT_LEVEL"); envVal != "" {
|
||||
level, err := strconv.Atoi(envVal)
|
||||
if err == nil && level >= 0 && level <= 5 {
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
// TF_VAR_ai_hint_level тоже поддерживаем
|
||||
if envVal := os.Getenv("TF_VAR_ai_hint_level"); envVal != "" {
|
||||
level, err := strconv.Atoi(envVal)
|
||||
if err == nil && level >= 0 && level <= 5 {
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
return 0 // по умолчанию — AI выключен
|
||||
}
|
||||
|
||||
// runTerraform запускает terraform с переданными аргументами.
|
||||
// Вывод идёт напрямую в stdout/stderr пользователя И захватывается для AI.
|
||||
// Возвращает текст вывода и код завершения.
|
||||
func runTerraform(args []string) (string, int) {
|
||||
cmd := exec.Command("terraform", args...)
|
||||
|
||||
// Захватываем stdout для AI, но также пишем напрямую в os.Stdout
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return buf.String(), exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "ошибка запуска terraform: %v\n", err)
|
||||
return buf.String(), 1
|
||||
}
|
||||
|
||||
return buf.String(), 0
|
||||
}
|
||||
|
||||
// runAIAnalysis запускает AI анализ и выводит результат.
|
||||
// Не прерывает работу если AI недоступен — только показывает предупреждение.
|
||||
func runAIAnalysis(level int, planOutput string) {
|
||||
cfg := ai.Config{
|
||||
HintLevel: level,
|
||||
Provider: getEnv("AI_PROVIDER", "google"),
|
||||
GoogleAPIKey: os.Getenv("GOOGLE_AI_API_KEY"),
|
||||
GoogleModel: getEnv("GOOGLE_AI_MODEL", "gemini-2.0-flash"),
|
||||
CloudEndpoint: os.Getenv("CLOUD_LLM_ENDPOINT"),
|
||||
CloudToken: os.Getenv("CLOUD_LLM_TOKEN"),
|
||||
}
|
||||
|
||||
analyzer, err := ai.NewAnalyzer(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "\n⚠ AI анализ недоступен: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result, err := analyzer.Analyze(ctx, planOutput)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "\n⚠ AI анализ завершился с ошибкой: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result != "" {
|
||||
fmt.Print(result)
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv возвращает значение env-переменной или defaultVal если не задана.
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
Reference in New Issue
Block a user