157 lines
5.3 KiB
Go
157 lines
5.3 KiB
Go
// 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 — провайдер: "groq" (по умолчанию), "google", "cloud"
|
|
// GROQ_API_KEY — API ключ Groq (рекомендуется, работает из РФ)
|
|
// GROQ_MODEL — модель Groq (по умолчанию "llama-3.3-70b-versatile")
|
|
// GROQ_ENDPOINT — endpoint (по умолчанию https://api.groq.com/openai/v1/chat/completions)
|
|
// GOOGLE_AI_API_KEY — API ключ Google Gemini (геоблок в РФ)
|
|
// GOOGLE_AI_MODEL — модель Gemini (по умолчанию "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", "groq"),
|
|
GroqAPIKey: os.Getenv("GROQ_API_KEY"),
|
|
GroqModel: getEnv("GROQ_MODEL", "llama-3.3-70b-versatile"),
|
|
GroqEndpoint: os.Getenv("GROQ_ENDPOINT"),
|
|
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
|
|
}
|