Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6960e87a5c | ||
|
|
11b484e1e7 | ||
|
|
ef7613d09f | ||
|
|
d9ed96e201 | ||
|
|
582b589e38 | ||
|
|
d5ce8c2fac | ||
|
|
a7fc960c82 | ||
|
|
cea13cf239 | ||
|
|
824dad7969 |
@@ -0,0 +1,156 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
# План: AI-анализ terraform plan (уровни 0–5)
|
||||||
|
|
||||||
|
Дата: 2026-03-12
|
||||||
|
Статус: черновик / на согласование
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Суть задачи
|
||||||
|
|
||||||
|
После выполнения `terraform plan` отправлять вывод на анализ в LLM.
|
||||||
|
Глубина анализа управляется переменной `ai_hint_level` (0–5) в Terraform-конфигурации.
|
||||||
|
Сейчас — Google AI (Gemini API). В будущем — собственный облачный LLM (drop-in замена).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Уровни подсказок (`ai_hint_level`)
|
||||||
|
|
||||||
|
| Уровень | Название | Что делает |
|
||||||
|
|---------|----------------|-------------------------------------------------------------------|
|
||||||
|
| 0 | OFF | Ничего. AI не вызывается. `terraform plan` работает как обычно. |
|
||||||
|
| 1 | SYNTAX | Проверка синтаксиса: ошибки в HCL, опечатки, невалидные блоки. |
|
||||||
|
| 2 | BASIC | + базовый анализ: неиспользуемые ресурсы, пустые значения. |
|
||||||
|
| 3 | MODERATE | + анализ зависимостей, потенциальные конфликты, порядок apply. |
|
||||||
|
| 4 | DETAILED | + best practices, безопасность (открытые порты, широкие IAM). |
|
||||||
|
| 5 | FULL | Полные рекомендации: оптимизация, рефакторинг, альтернативы. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Как задаётся уровень
|
||||||
|
|
||||||
|
**По умолчанию AI не активен и ничего не показывает.** Если `ai_hint_level` нигде не задан — уровень 0, `sless plan` работает как обычный `terraform plan`, без единого упоминания об AI.
|
||||||
|
|
||||||
|
Уровень задаётся **только явно**, одним из способов (приоритет сверху вниз):
|
||||||
|
|
||||||
|
1. Env-переменная (рекомендуется):
|
||||||
|
```bash
|
||||||
|
export TF_VAR_ai_hint_level=3
|
||||||
|
```
|
||||||
|
|
||||||
|
2. В `variables.tf` пользовательского проекта (опционально, только если нужно зафиксировать уровень в коде):
|
||||||
|
```hcl
|
||||||
|
variable "ai_hint_level" {
|
||||||
|
description = "Уровень AI-анализа terraform plan (0=off, 5=full)"
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = var.ai_hint_level >= 0 && var.ai_hint_level <= 5
|
||||||
|
error_message = "ai_hint_level должен быть от 0 до 5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. CLI-флаг напрямую:
|
||||||
|
```bash
|
||||||
|
sless plan --ai-level=3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** CLI сам читает уровень из env `TF_VAR_ai_hint_level` или флага `--ai-level`. Если ни того ни другого нет — молча применяет уровень 0. Никакой переменной в манифестах не требуется. Пользователь не видит AI, пока не включит его явно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Архитектура решения
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ terraform │────>│ wrapper-скрипт │────>│ AI Analyzer │
|
||||||
|
│ plan │ │ (bash/Go CLI) │ │ (Go сервис) │
|
||||||
|
│ (вывод JSON)│ │ │ │ │
|
||||||
|
└─────────────┘ └──────────────────┘ └────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ LLM Backend │
|
||||||
|
│ (Google Gemini) │
|
||||||
|
│ → Cloud LLM │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Компоненты:
|
||||||
|
|
||||||
|
1. **Wrapper-скрипт / CLI** — вызывает `terraform plan -json`, читает `ai_hint_level`, передаёт вывод в анализатор.
|
||||||
|
2. **AI Analyzer (Go)** — формирует промпт по уровню, отправляет в LLM, форматирует ответ.
|
||||||
|
3. **LLM Backend** — подключаемый через интерфейс (сначала Google, потом облачный).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Реализация по шагам
|
||||||
|
|
||||||
|
### Шаг 1: CLI-обёртка (`sless-plan`)
|
||||||
|
|
||||||
|
Новый бинарник или shell-скрипт, который:
|
||||||
|
- Запускает `terraform plan -json -out=plan.bin`
|
||||||
|
- Читает `ai_hint_level` из state/variables (через `terraform output` или парсинг .tf)
|
||||||
|
- Если level > 0, передаёт JSON-вывод плана в AI Analyzer
|
||||||
|
- Выводит стандартный plan + AI-рекомендации ниже
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Пользователь вызывает:
|
||||||
|
sless plan # вместо terraform plan
|
||||||
|
# или:
|
||||||
|
terraform plan && sless analyze --level=3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: AI Analyzer сервис (Go)
|
||||||
|
|
||||||
|
Расположение: `internal/ai/` или отдельный `cmd/ai-analyzer/`
|
||||||
|
|
||||||
|
```
|
||||||
|
internal/ai/
|
||||||
|
├── analyzer.go # основная логика: формирование промпта, парсинг ответа
|
||||||
|
├── provider.go # интерфейс LLM-провайдера
|
||||||
|
├── google_gemini.go # реализация для Google Gemini API
|
||||||
|
├── cloud_llm.go # (заглушка) реализация для будущего облачного LLM
|
||||||
|
└── prompts.go # шаблоны промптов по уровням 1–5
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключевой интерфейс:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// LLMProvider — абстракция над любым LLM-бэкендом.
|
||||||
|
// Сейчас: Google Gemini. В будущем: облачный LLM (drop-in замена).
|
||||||
|
type LLMProvider interface {
|
||||||
|
Analyze(ctx context.Context, prompt string) (string, error)
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3: Промпты по уровням
|
||||||
|
|
||||||
|
Каждый уровень = свой system prompt + ограничения:
|
||||||
|
|
||||||
|
| Уровень | System prompt (суть) |
|
||||||
|
|---------|-----------------------------------------------------------|
|
||||||
|
| 1 | "Проверь только синтаксис HCL. Ничего лишнего." |
|
||||||
|
| 2 | "Синтаксис + базовые проблемы (unused, empty values)." |
|
||||||
|
| 3 | "Анализ зависимостей, порядок, потенциальные конфликты." |
|
||||||
|
| 4 | "Best practices, безопасность, IAM, открытые порты." |
|
||||||
|
| 5 | "Полный аудит: оптимизация, рефакторинг, альтернативы." |
|
||||||
|
|
||||||
|
### Шаг 4: Google Gemini интеграция
|
||||||
|
|
||||||
|
- API: `generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent`
|
||||||
|
- Авторизация: API key (хранить в env `GOOGLE_AI_API_KEY`, не в коде)
|
||||||
|
- SDK: `github.com/google/generative-ai-go` (официальный Go SDK)
|
||||||
|
- Модель: `gemini-pro` или `gemini-1.5-pro`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// google_gemini.go — реализует LLMProvider
|
||||||
|
type GeminiProvider struct {
|
||||||
|
client *genai.Client
|
||||||
|
model string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 5: Конфигурация
|
||||||
|
|
||||||
|
Через env-переменные (не хардкод):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Какой LLM-провайдер использовать
|
||||||
|
export AI_PROVIDER=google # google | cloud (в будущем)
|
||||||
|
|
||||||
|
# Google Gemini
|
||||||
|
export GOOGLE_AI_API_KEY=AIza... # API ключ
|
||||||
|
|
||||||
|
# Будущий облачный LLM
|
||||||
|
export CLOUD_LLM_ENDPOINT=https://llm.cloud.example.com/v1/analyze
|
||||||
|
export CLOUD_LLM_TOKEN=...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Миграция на облачный LLM
|
||||||
|
|
||||||
|
Когда облако предоставит свой LLM:
|
||||||
|
|
||||||
|
1. Создать `cloud_llm.go` — реализация `LLMProvider` для нового API
|
||||||
|
2. Переключить `AI_PROVIDER=cloud` в env
|
||||||
|
3. Код анализатора НЕ меняется — интерфейс `LLMProvider` тот же
|
||||||
|
4. Google Gemini остаётся как fallback (опционально)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Фабрика провайдеров — выбор по переменной окружения
|
||||||
|
func NewProvider(cfg Config) (LLMProvider, error) {
|
||||||
|
switch cfg.Provider {
|
||||||
|
case "google":
|
||||||
|
return NewGeminiProvider(cfg.GoogleAPIKey)
|
||||||
|
case "cloud":
|
||||||
|
return NewCloudLLMProvider(cfg.CloudEndpoint, cfg.CloudToken)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown AI provider: %s", cfg.Provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Безопасность
|
||||||
|
|
||||||
|
- API-ключи — только в env/secrets, никогда в коде или .tf файлах
|
||||||
|
- `terraform plan -json` может содержать sensitive data → фильтровать перед отправкой в LLM
|
||||||
|
- Ограничить размер payload (большие планы обрезать/суммаризировать)
|
||||||
|
- Логировать факт вызова AI, но НЕ содержимое запроса/ответа в production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Структура файлов (итого)
|
||||||
|
|
||||||
|
```
|
||||||
|
sless/
|
||||||
|
├── cmd/
|
||||||
|
│ └── sless-plan/ # CLI-обёртка для terraform plan + AI
|
||||||
|
│ └── main.go
|
||||||
|
├── internal/
|
||||||
|
│ └── ai/
|
||||||
|
│ ├── analyzer.go # логика анализа
|
||||||
|
│ ├── provider.go # интерфейс LLMProvider
|
||||||
|
│ ├── google_gemini.go # Google Gemini реализация
|
||||||
|
│ ├── cloud_llm.go # заглушка для облачного LLM
|
||||||
|
│ └── prompts.go # промпты по уровням 1–5
|
||||||
|
├── terraform/
|
||||||
|
│ └── provider/ # (существующий провайдер — без изменений)
|
||||||
|
└── examples/
|
||||||
|
└── */
|
||||||
|
└── variables.tf # добавить ai_hint_level variable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Порядок реализации
|
||||||
|
|
||||||
|
| # | Задача | Приоритет |
|
||||||
|
|----|------------------------------------------------|-----------|
|
||||||
|
| 1 | Создать `internal/ai/provider.go` (интерфейс) | HIGH |
|
||||||
|
| 2 | Реализовать `google_gemini.go` | HIGH |
|
||||||
|
| 3 | Написать промпты по уровням (`prompts.go`) | HIGH |
|
||||||
|
| 4 | Создать `analyzer.go` (оркестрация) | HIGH |
|
||||||
|
| 5 | CLI-обёртка `cmd/sless-plan/main.go` | HIGH |
|
||||||
|
| 6 | Добавить `ai_hint_level` в examples | MEDIUM |
|
||||||
|
| 7 | Тесты (unit + integration с mock LLM) | MEDIUM |
|
||||||
|
| 8 | Заглушка `cloud_llm.go` для будущей миграции | LOW |
|
||||||
|
| 9 | Документация для пользователей | MEDIUM |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Пример использования (целевой UX)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# В terraform проекте пользователя:
|
||||||
|
$ export TF_VAR_ai_hint_level=3
|
||||||
|
$ export GOOGLE_AI_API_KEY=AIza...
|
||||||
|
|
||||||
|
$ sless plan
|
||||||
|
# Terraform Plan output (стандартный)
|
||||||
|
# ...
|
||||||
|
# ──────────────── AI Analysis (level 3: MODERATE) ────────────────
|
||||||
|
# ⚠ Resource "kubernetes_deployment.func" зависит от "kubernetes_namespace.ns"
|
||||||
|
# но namespace создаётся в другом модуле — возможен race condition.
|
||||||
|
# ⚠ Порядок destroy может вызвать проблемы: trigger удалится раньше function.
|
||||||
|
# ✓ Синтаксис корректен, конфликтов не обнаружено.
|
||||||
|
# ─────────────────────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Решение зафиксировано
|
||||||
|
|
||||||
|
Подход: **интерфейс LLMProvider** → сначала Google Gemini → потом drop-in замена на облачный LLM.
|
||||||
|
Уровень задаётся через обычную Terraform variable. Никаких изменений в самом провайдере sless.
|
||||||
@@ -646,3 +646,88 @@ for _, k := range keys { envVars = append(envVars, corev1.EnvVar{Name: k, Value:
|
|||||||
|
|
||||||
**Тесты:** 2 теста в `controllers/function_controller_unit_test.go`
|
**Тесты:** 2 теста в `controllers/function_controller_unit_test.go`
|
||||||
(4 env vars → алфавитный порядок после SLESS_ENTRYPOINT; пустой Env → только SLESS_ENTRYPOINT).
|
(4 env vars → алфавитный порядок после SLESS_ENTRYPOINT; пустой Env → только SLESS_ENTRYPOINT).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-12 — AI-анализ terraform plan через Google Gemini
|
||||||
|
|
||||||
|
**Решение:** Добавлен необязательный AI-анализ вывода `terraform plan` с уровнями детализации 0–5.
|
||||||
|
АI невидим по умолчанию — не влияет на существующие `.tf` файлы и workflow.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
- Разработчики хотят понимать что именно изменится в инфраструктуре перед применением.
|
||||||
|
- Уровни позволяют самостоятельно выбирать глубину анализа (от краткого обзора до полного аудита).
|
||||||
|
- Не ломает backward compatibility: `ai_hint_level` не указан → поведение идентично прежнему.
|
||||||
|
|
||||||
|
**Архитектура:**
|
||||||
|
```
|
||||||
|
provider "sless" { ai_hint_level = 3 }
|
||||||
|
↓ os.Setenv("AI_HINT_LEVEL", "3") (в provider.Configure)
|
||||||
|
sless-plan ← CLI обёртка над terraform plan
|
||||||
|
↓ запускает terraform plan, захватывает вывод
|
||||||
|
↓ если AI_HINT_LEVEL > 0 → internal/ai/analyzer.go
|
||||||
|
↓ POST к Gemini REST API (net/http)
|
||||||
|
↓ выводит анализ пользователю
|
||||||
|
```
|
||||||
|
|
||||||
|
**Новые файлы:**
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|------------|
|
||||||
|
| `internal/ai/provider.go` | `LLMProvider` interface + `Config` struct + `NewProvider` фабрика |
|
||||||
|
| `internal/ai/google_gemini.go` | Gemini 2.0 Flash через `net/http` (без внешних зависимостей) |
|
||||||
|
| `internal/ai/prompts.go` | Системные промпты для уровней 1–5, `BuildPrompt(level, plan)`, `LevelName(level)` |
|
||||||
|
| `internal/ai/analyzer.go` | Оркестратор: `NewAnalyzer`, `Analyze`, `FormatResult`, `truncatePlan(8000)` |
|
||||||
|
| `internal/ai/cloud_llm.go` | Заглушка: миграционный путь на облачный LLM в будущем |
|
||||||
|
| `cmd/sless-plan/main.go` | CLI: запускает `terraform plan`, читает уровень, вызывает AI |
|
||||||
|
|
||||||
|
**Изменённые файлы:**
|
||||||
|
- `terraform/provider/internal/provider/provider.go` — добавлен атрибут `ai_hint_level` (Optional Int64)
|
||||||
|
|
||||||
|
**Уровни анализа (env: `AI_HINT_LEVEL`):**
|
||||||
|
| Уровень | Описание |
|
||||||
|
|---------|----------|
|
||||||
|
| 0 | Выключен (default) — AI не вызывается |
|
||||||
|
| 1 | Только синтаксические ошибки и опечатки |
|
||||||
|
| 2 | Базовый обзор изменений |
|
||||||
|
| 3 | Умеренный: потенциальные проблемы + рекомендации |
|
||||||
|
| 4 | Детальный: security review + best practices |
|
||||||
|
| 5 | Полный аудит: всё включая compliance |
|
||||||
|
|
||||||
|
**Как передаётся уровень (приоритет по убыванию):**
|
||||||
|
1. `--ai-level` флаг CLI
|
||||||
|
2. `AI_HINT_LEVEL` env var (выставляется провайдером через `os.Setenv` в `Configure`)
|
||||||
|
3. `TF_VAR_ai_hint_level` env var
|
||||||
|
4. Дефолт: 0
|
||||||
|
|
||||||
|
**Почему `net/http` без Google SDK:**
|
||||||
|
Gemini SDK требует внешней зависимости → `go.mod` провайдера должен оставаться неизменным.
|
||||||
|
REST API Gemini достаточен: один endpoint, один JSON запрос/ответ.
|
||||||
|
|
||||||
|
**Backward compatibility:**
|
||||||
|
- Провайдер без `ai_hint_level` → `config.AIHintLevel.IsNull() == true` → `os.Setenv` не вызывается.
|
||||||
|
- `sless-plan` без `AI_HINT_LEVEL` → уровень 0 → AI не вызывается → вывод идентичен `terraform plan`.
|
||||||
|
- Существующие `.tf` файлы изменять не нужно.
|
||||||
|
|
||||||
|
**Коммиты:** `d5ce8c2` (internal/ai + cmd/sless-plan), `582b589` (provider schema)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-12 — Groq/OpenAI-compat как default AI провайдер, self-hosted LLM не поднимаем
|
||||||
|
|
||||||
|
**Решение:** Groq — default AI провайдер (через OpenAI-compat API). Google Gemini — альтернатива (env `AI_PROVIDER=google`). Self-hosted LLM (ollama) — не устанавливаем на VM.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
- `sless-plan` — **клиентский инструмент**. Запускается на машине разработчика, не на сервере оператора. Разработчик сам отвечает за наличие доступа к AI API.
|
||||||
|
- Self-hosted LLM на VM облака — чужая инфраструктура, нецелевое использование ресурсов, потенциальные трения с облачной командой.
|
||||||
|
- Groq — бесплатный tier, OpenAI-compat API (минимальный код), модель `llama-3.3-70b-versatile` достаточна для code review.
|
||||||
|
|
||||||
|
**Будущее:** когда облако запустит свой LLM endpoint — добавить case `"cloud"` в `NewProvider`, endpoint из env. Stub `internal/ai/cloud_llm.go` уже есть.
|
||||||
|
|
||||||
|
**Переменные окружения для разработчика:**
|
||||||
|
```bash
|
||||||
|
export GROQ_API_KEY=gsk_... # обязательно для Groq
|
||||||
|
export AI_PROVIDER=groq # default, можно опустить
|
||||||
|
export GROQ_MODEL=llama-3.3-70b-versatile # default, можно опустить
|
||||||
|
```
|
||||||
|
|
||||||
|
**Коммит:** `11b484e`
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Работа с удалённой машиной Nubes
|
||||||
|
|
||||||
|
Дата: 2026-03-12
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Локальный терминал Copilot нестабилен из-за VPN — команды зависают, таймауты.
|
||||||
|
**Весь код физически живёт на удалённой Ubuntu VM**, доступной по SSH.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
Локальная машина (Krupski)
|
||||||
|
└── SSHFS → /home/naeel/remote_dev → naeel@5.172.178.213:/home/naeel/terra
|
||||||
|
└── sless/ ← реальный репозиторий
|
||||||
|
```
|
||||||
|
|
||||||
|
## Подключение к VM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Хост
|
||||||
|
VM_HOST=naeel@5.172.178.213
|
||||||
|
VM_KEY=/home/naeel/.ssh/id_ed25519 # ~/.ssh/id_ed25519 = копия secrets/naeel_vm_id_ed25519
|
||||||
|
|
||||||
|
# Проверить подключение
|
||||||
|
ssh -i /home/naeel/.ssh/id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 "hostname && whoami"
|
||||||
|
|
||||||
|
# Выполнить команду удалённо
|
||||||
|
ssh -i /home/naeel/.ssh/id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 "cd /home/naeel/terra/sless && <команда>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Путь к репозиторию на VM
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/naeel/terra/sless
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git push через SSH (Gitea)
|
||||||
|
|
||||||
|
Remote origin использует HTTPS → нужен SSH для беспарольного пуша.
|
||||||
|
|
||||||
|
На VM уже лежит ключ `~/.ssh/gitea_key` (тот же ключ, что `id_ed25519`, зарегистрирован в Gitea).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Пуш с VM
|
||||||
|
ssh -i /home/naeel/.ssh/id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 \
|
||||||
|
'cd /home/naeel/terra/sless && \
|
||||||
|
GIT_SSH_COMMAND="ssh -i ~/.ssh/gitea_key -o StrictHostKeyChecking=no" \
|
||||||
|
git push git@gitea-naeel.giteak8s.services.ngcloud.ru:naeel/sless.git <branch>'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea SSH ключи
|
||||||
|
|
||||||
|
Ключ `~/.ssh/id_ed25519` зарегистрирован в Gitea:
|
||||||
|
- Email: tazet@narod.ru
|
||||||
|
- Fingerprint: `SHA256:WUAvQXXmRUr6jHHdkMDljKKBUEQ8ot2cTU3YSixqYe4`
|
||||||
|
|
||||||
|
## Типовой git workflow (commit + push)
|
||||||
|
|
||||||
|
Токен лежит в `secrets/gitea.token` (Gitea PAT, repository read+write).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(cat /home/naeel/remote_dev/sless/secrets/gitea.token | tr -d '\n')
|
||||||
|
ssh -i /home/naeel/.ssh/id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 "
|
||||||
|
cd /home/naeel/terra/sless
|
||||||
|
git add <файлы>
|
||||||
|
git commit -m '<сообщение>'
|
||||||
|
git push https://naeel:${TOKEN}@gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless.git <branch>
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSHFS монтирование (восстановить если упало)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshfs \
|
||||||
|
-o IdentityFile=/home/naeel/.ssh/id_ed25519 \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
-o UserKnownHostsFile=/dev/null \
|
||||||
|
-o reconnect \
|
||||||
|
-o ServerAliveInterval=15 \
|
||||||
|
-o ServerAliveCountMax=3 \
|
||||||
|
naeel@5.172.178.213:/home/naeel/terra /home/naeel/remote_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
- `secrets/naeel_vm_id_ed25519` и `~/.ssh/id_ed25519` — одинаковые ключи (SHA256 совпадает)
|
||||||
|
- При зависании локального терминала — выполнять команды через SSH на VM напрямую
|
||||||
|
- Git config на VM настроен: `user.name=Naeel`, `user.email=naeel@nubes.ru`
|
||||||
@@ -230,6 +230,37 @@
|
|||||||
| 6 | `replicas` field в FunctionSpec | Низкий (v1.1) |
|
| 6 | `replicas` field в FunctionSpec | Низкий (v1.1) |
|
||||||
| 7 | RabbitMQ event triggers | Низкий (v2) |
|
| 7 | RabbitMQ event triggers | Низкий (v2) |
|
||||||
| 8 | Метрики → Victoria Metrics | Низкий |
|
| 8 | Метрики → Victoria Metrics | Низкий |
|
||||||
|
| 9 | `sless-plan` реально не запускает `terraform plan` — нужна интеграция в workflow (docs) | Средний |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-12 — AI-анализ terraform plan (ветка feat/ai-terraform-analysis)
|
||||||
|
|
||||||
|
| # | Компонент | Статус | Заметки |
|
||||||
|
|---|-----------|--------|----------|
|
||||||
|
| 1 | `internal/ai/provider.go` | ✅ | `LLMProvider` interface + `Config` struct + `NewProvider` фабрика |
|
||||||
|
| 2 | `internal/ai/google_gemini.go` | ✅ | Gemini REST API через `net/http` (без внешних SDK) |
|
||||||
|
| 3 | `internal/ai/prompts.go` | ✅ | Системные промпты уровней 1–5, `BuildPrompt`, `LevelName` |
|
||||||
|
| 4 | `internal/ai/analyzer.go` | ✅ | Оркестратор: `NewAnalyzer`, `Analyze`, `FormatResult`, `truncatePlan(8000)` |
|
||||||
|
| 5 | `internal/ai/cloud_llm.go` | ✅ | Заглушка для будущего облачного LLM |
|
||||||
|
| 6 | `cmd/sless-plan/main.go` | ✅ | CLI обёртка над `terraform plan` + AI анализ после |
|
||||||
|
| 7 | `terraform/provider/internal/provider/provider.go` | ✅ | `ai_hint_level` атрибут (опциональный Int64, default=0) в Schema и Configure |
|
||||||
|
| 8 | `go build ./internal/ai/... && go build ./cmd/sless-plan/...` | ✅ | BUILD OK на VM |
|
||||||
|
| 9 | `go build ./...` в terraform/provider/ | ✅ | BUILD OK на VM |
|
||||||
|
| 10 | Коммит + пуш | ✅ | `d5ce8c2` (ai pkg + cmd) → `582b589` (provider) |
|
||||||
|
|
||||||
|
**Ветка:** `feat/ai-terraform-analysis`
|
||||||
|
**Последний коммит:** `582b589`
|
||||||
|
|
||||||
|
**Как использовать:**
|
||||||
|
```hcl
|
||||||
|
provider "sless" {
|
||||||
|
endpoint = "https://sless-api.kube5s.ru"
|
||||||
|
token = var.token
|
||||||
|
ai_hint_level = 3 # 0=off (default), 1–5 = уровень анализа
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Затем вместо `terraform plan` использовать `sless-plan` — предоставит анализ от Gemini.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Инструкции: монтирование удалённой папки через SSHFS
|
||||||
|
|
||||||
|
Дата: 2026-03-12
|
||||||
|
|
||||||
|
Кратко: эти команды повторяют текущее у вас подключение, где
|
||||||
|
источник: `naeel@5.172.178.213:/home/naeel/terra`, и точка монтирования — `/home/<user>/remote_dev`.
|
||||||
|
|
||||||
|
1) Установка `sshfs` (Debian/Ubuntu):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y sshfs
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Подготовка приватного ключа:
|
||||||
|
|
||||||
|
- Скопируйте приватный ключ на целевую машину или загрузите его безопасным способом.
|
||||||
|
- Пример копирования с локальной машины на целевой хост:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp /home/naeel/remote_dev/sless/secrets/naeel_vm_id_ed25519 user@target:/home/user/.ssh/id_ed25519_sless
|
||||||
|
ssh user@target 'chmod 600 /home/user/.ssh/id_ed25519_sless'
|
||||||
|
```
|
||||||
|
|
||||||
|
(Оригинал ключа в репозитории: `secrets/naeel_vm_id_ed25519`)
|
||||||
|
|
||||||
|
3) Создать точку монтирования на целевом хосте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /home/user/remote_dev
|
||||||
|
chown user:user /home/user/remote_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4) Команда монтирования (пример, повторяет ваше текущее подключение):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshfs -o IdentityFile=/home/user/.ssh/id_ed25519_sless \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
-o UserKnownHostsFile=/dev/null \
|
||||||
|
-o reconnect \
|
||||||
|
-o ServerAliveInterval=15 \
|
||||||
|
-o ServerAliveCountMax=3 \
|
||||||
|
naeel@5.172.178.213:/home/naeel/terra /home/user/remote_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5) Проверка статуса монтирования и содержимого:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
findmnt /home/user/remote_dev
|
||||||
|
ls -la /home/user/remote_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
6) Отключение:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fusermount -u /home/user/remote_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
7) (Опционально) systemd unit для автоподключения при старте:
|
||||||
|
|
||||||
|
Создайте файл `/etc/systemd/system/remote_dev.mount` со следующим содержимым (отредактируйте путь к ключу и `Where`):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=SSHFS mount for /home/user/remote_dev
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Mount]
|
||||||
|
What=naeel@5.172.178.213:/home/naeel/terra
|
||||||
|
Where=/home/user/remote_dev
|
||||||
|
Type=fuse.sshfs
|
||||||
|
Options=IdentityFile=/home/user/.ssh/id_ed25519_sless,allow_other,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,StrictHostKeyChecking=no,UserKnownHostsFile=/dev/null
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем выполнить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now remote_dev.mount
|
||||||
|
```
|
||||||
|
|
||||||
|
8) Заметки по безопасности и надёжности:
|
||||||
|
|
||||||
|
- Храните приватный ключ с правами `600` и ограниченным доступом.
|
||||||
|
- Рассмотрите использование `ssh-agent` вместо копирования ключа.
|
||||||
|
- Опция `StrictHostKeyChecking=no` ослабляет проверку host key — используйте осознанно.
|
||||||
|
- Для массовой настройки можете создать скрипт установки и unit-файл через Ansible/Cloud-init.
|
||||||
|
|
||||||
|
---
|
||||||
|
Файл создан автоматически по запросу пользователя. Если хотите, могу:
|
||||||
|
- добавить инструкции для `macOS` или `CentOS`/`RHEL`;
|
||||||
|
- создать такой же unit-файл и загрузить его на целевой хост (при доступе по SSH).
|
||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
sless = {
|
sless = {
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
version = "~> 0.1.16"
|
version = "~> 0.1.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
sless = {
|
sless = {
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
version = "~> 0.1.16"
|
version = "~> 0.1.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ terraform {
|
|||||||
# Провайдер для управления serverless функциями через sless API
|
# Провайдер для управления serverless функциями через sless API
|
||||||
sless = {
|
sless = {
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
version = "~> 0.1.16"
|
version = "~> 0.1.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
sless = {
|
sless = {
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
version = "~> 0.1.16"
|
version = "~> 0.1.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,4 +14,5 @@ provider "sless" {
|
|||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless-api.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||||||
|
ai_hint_level = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
sless = {
|
sless = {
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
version = "~> 0.1.16"
|
version = "~> 0.1.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
sless = {
|
sless = {
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
version = "~> 0.1.16"
|
version = "~> 0.1.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// internal/ai/analyzer.go
|
||||||
|
// Дата: 2026-03-12
|
||||||
|
// Оркестратор AI-анализа: читает уровень, формирует промпт,
|
||||||
|
// вызывает LLM-провайдер, форматирует вывод для пользователя.
|
||||||
|
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Analyzer — основной компонент AI-анализа.
|
||||||
|
// Создаётся один раз при старте sless-plan, используется для одного анализа.
|
||||||
|
type Analyzer struct {
|
||||||
|
provider LLMProvider
|
||||||
|
level int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnalyzer создаёт Analyzer из конфига.
|
||||||
|
// Возвращает ошибку если провайдер не может быть инициализирован.
|
||||||
|
func NewAnalyzer(cfg Config) (*Analyzer, error) {
|
||||||
|
if cfg.HintLevel == 0 {
|
||||||
|
// Уровень 0 — AI выключен, провайдер не нужен
|
||||||
|
return &Analyzer{level: 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка инициализации AI провайдера: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Analyzer{
|
||||||
|
provider: provider,
|
||||||
|
level: cfg.HintLevel,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled возвращает true если уровень > 0 и провайдер инициализирован.
|
||||||
|
func (a *Analyzer) IsEnabled() bool {
|
||||||
|
return a.level > 0 && a.provider != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze отправляет вывод terraform plan в LLM и возвращает отформатированный результат.
|
||||||
|
// planText — текстовый вывод terraform plan.
|
||||||
|
func (a *Analyzer) Analyze(ctx context.Context, planText string) (string, error) {
|
||||||
|
if !a.IsEnabled() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем размер payload — большие планы обрезаем
|
||||||
|
// чтобы не превысить лимит токенов LLM и не отправлять лишнее
|
||||||
|
planText = truncatePlan(planText, 8000)
|
||||||
|
|
||||||
|
prompt := BuildPrompt(a.level, planText)
|
||||||
|
result, err := a.provider.Analyze(ctx, prompt)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("AI анализ не удался: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormatResult(a.level, a.provider.Name(), result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatResult оборачивает ответ LLM в читаемый блок для вывода в терминал.
|
||||||
|
func FormatResult(level int, providerName, result string) string {
|
||||||
|
separator := strings.Repeat("─", 60)
|
||||||
|
header := fmt.Sprintf("AI Analysis (level %d: %s) via %s", level, LevelName(level), providerName)
|
||||||
|
return fmt.Sprintf("\n%s\n %s\n%s\n%s\n%s\n",
|
||||||
|
separator,
|
||||||
|
header,
|
||||||
|
separator,
|
||||||
|
result,
|
||||||
|
separator,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncatePlan обрезает текст плана до maxChars символов с пометкой об обрезке.
|
||||||
|
// Нужно чтобы не превышать лимиты токенов LLM и не отправлять лишние данные.
|
||||||
|
func truncatePlan(text string, maxChars int) string {
|
||||||
|
if len(text) <= maxChars {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text[:maxChars] + fmt.Sprintf("\n\n[... обрезано, показаны первые %d символов из %d ...]", maxChars, len(text))
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// internal/ai/cloud_llm.go
|
||||||
|
// Дата: 2026-03-12
|
||||||
|
// Заглушка для будущего облачного LLM.
|
||||||
|
// Когда облако предоставит свой LLM — реализовать этот файл
|
||||||
|
// и переключить AI_PROVIDER=cloud. Остальной код не меняется.
|
||||||
|
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloudLLMProvider — будущая реализация LLMProvider для облачного LLM.
|
||||||
|
// Пока возвращает ошибку "не реализован".
|
||||||
|
type CloudLLMProvider struct {
|
||||||
|
endpoint string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCloudLLMProvider создаёт заглушку провайдера облачного LLM.
|
||||||
|
func NewCloudLLMProvider(endpoint, token string) (*CloudLLMProvider, error) {
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil, fmt.Errorf("CLOUD_LLM_ENDPOINT не задан")
|
||||||
|
}
|
||||||
|
return &CloudLLMProvider{
|
||||||
|
endpoint: endpoint,
|
||||||
|
token: token,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloudLLMProvider) Name() string {
|
||||||
|
return fmt.Sprintf("cloud-llm/%s", c.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze — TODO: реализовать когда облако предоставит LLM API.
|
||||||
|
// Документация по API будет в doc/api/cloud-llm.md.
|
||||||
|
func (c *CloudLLMProvider) Analyze(_ context.Context, _ string) (string, error) {
|
||||||
|
return "", fmt.Errorf("cloud LLM провайдер ещё не реализован (endpoint: %s)", c.endpoint)
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// internal/ai/google_gemini.go
|
||||||
|
// Дата: 2026-03-12
|
||||||
|
// Реализация LLMProvider для Google Gemini API.
|
||||||
|
// Использует REST API напрямую через net/http — без внешних SDK,
|
||||||
|
// чтобы не нарушать совместимость существующего go.mod.
|
||||||
|
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const geminiBaseURL = "https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent"
|
||||||
|
|
||||||
|
// GeminiProvider реализует LLMProvider через Google Gemini REST API.
|
||||||
|
type GeminiProvider struct {
|
||||||
|
apiKey string
|
||||||
|
model string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGeminiProvider создаёт провайдера для Google Gemini.
|
||||||
|
// apiKey — значение env GOOGLE_AI_API_KEY.
|
||||||
|
func NewGeminiProvider(apiKey, model string) (*GeminiProvider, error) {
|
||||||
|
if apiKey == "" {
|
||||||
|
return nil, fmt.Errorf("GOOGLE_AI_API_KEY не задан")
|
||||||
|
}
|
||||||
|
return &GeminiProvider{
|
||||||
|
apiKey: apiKey,
|
||||||
|
model: model,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GeminiProvider) Name() string {
|
||||||
|
return fmt.Sprintf("google-gemini/%s", g.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// geminiRequest — структура запроса к Gemini API.
|
||||||
|
type geminiRequest struct {
|
||||||
|
Contents []geminiContent `json:"contents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type geminiContent struct {
|
||||||
|
Parts []geminiPart `json:"parts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type geminiPart struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// geminiResponse — структура ответа от Gemini API.
|
||||||
|
type geminiResponse struct {
|
||||||
|
Candidates []struct {
|
||||||
|
Content struct {
|
||||||
|
Parts []struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"parts"`
|
||||||
|
} `json:"content"`
|
||||||
|
} `json:"candidates"`
|
||||||
|
Error *struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze отправляет промпт в Gemini и возвращает текстовый ответ.
|
||||||
|
func (g *GeminiProvider) Analyze(ctx context.Context, prompt string) (string, error) {
|
||||||
|
reqBody := geminiRequest{
|
||||||
|
Contents: []geminiContent{
|
||||||
|
{Parts: []geminiPart{{Text: prompt}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf(geminiBaseURL+"?key=%s", g.model, g.apiKey)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка создания HTTP запроса: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := g.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка HTTP запроса к Gemini: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gemResp geminiResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &gemResp); err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка парсинга ответа Gemini: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем HTTP-ошибку и ошибку в теле ответа
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
msg := fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||||
|
if gemResp.Error != nil {
|
||||||
|
msg = gemResp.Error.Message
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Gemini API ошибка: %s", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gemResp.Candidates) == 0 || len(gemResp.Candidates[0].Content.Parts) == 0 {
|
||||||
|
return "", fmt.Errorf("Gemini вернул пустой ответ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gemResp.Candidates[0].Content.Parts[0].Text, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// internal/ai/openai_compat.go
|
||||||
|
// Дата: 2026-03-12
|
||||||
|
// OpenAI-совместимый провайдер — работает с Groq, OpenRouter, OpenAI и любым
|
||||||
|
// другим сервисом реализующим /v1/chat/completions API.
|
||||||
|
// Нет геоблока в отличие от Google Gemini.
|
||||||
|
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenAICompatProvider реализует LLMProvider через OpenAI-совместимый API.
|
||||||
|
// Groq endpoint: https://api.groq.com/openai/v1/chat/completions
|
||||||
|
type OpenAICompatProvider struct {
|
||||||
|
endpoint string
|
||||||
|
apiKey string
|
||||||
|
model string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOpenAICompatProvider создаёт провайдера для любого OpenAI-совместимого API.
|
||||||
|
// endpoint — полный URL до /v1/chat/completions
|
||||||
|
// apiKey — Bearer токен (GROQ_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY)
|
||||||
|
// model — название модели (например "llama-3.3-70b-versatile" для Groq)
|
||||||
|
func NewOpenAICompatProvider(endpoint, apiKey, model string) (*OpenAICompatProvider, error) {
|
||||||
|
if apiKey == "" {
|
||||||
|
return nil, fmt.Errorf("GROQ_API_KEY не задан")
|
||||||
|
}
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
model = "llama-3.3-70b-versatile"
|
||||||
|
}
|
||||||
|
return &OpenAICompatProvider{
|
||||||
|
endpoint: endpoint,
|
||||||
|
apiKey: apiKey,
|
||||||
|
model: model,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OpenAICompatProvider) Name() string {
|
||||||
|
return fmt.Sprintf("groq/%s", p.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// openAIRequest — тело запроса к /v1/chat/completions
|
||||||
|
type openAIRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []openAIMessage `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openAIMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// openAIResponse — тело ответа от /v1/chat/completions
|
||||||
|
type openAIResponse struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
Error *struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze отправляет промпт в OpenAI-совместимый API и возвращает ответ.
|
||||||
|
func (p *OpenAICompatProvider) Analyze(ctx context.Context, prompt string) (string, error) {
|
||||||
|
reqBody := openAIRequest{
|
||||||
|
Model: p.model,
|
||||||
|
Messages: []openAIMessage{
|
||||||
|
{Role: "user", Content: prompt},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("marshal error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create request error: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||||
|
|
||||||
|
resp, err := p.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("http error: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read response error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed openAIResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &parsed); err != nil {
|
||||||
|
return "", fmt.Errorf("parse response error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Error != nil {
|
||||||
|
return "", fmt.Errorf("API ошибка: %s", parsed.Error.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsed.Choices) == 0 || parsed.Choices[0].Message.Content == "" {
|
||||||
|
return "", fmt.Errorf("пустой ответ от API")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Choices[0].Message.Content, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// internal/ai/prompts.go
|
||||||
|
// Дата: 2026-03-12
|
||||||
|
// Шаблоны system-промптов для каждого уровня анализа (1–5).
|
||||||
|
// Уровень 0 — AI не вызывается вообще (проверка в analyzer.go).
|
||||||
|
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// systemPrompts — system prompt для каждого уровня.
|
||||||
|
// Каждый уровень строго ограничен своей областью — не расширять без явного указания.
|
||||||
|
var systemPrompts = map[int]string{
|
||||||
|
1: `Ты — анализатор Terraform конфигурации. Твоя задача: ТОЛЬКО синтаксический анализ.
|
||||||
|
Найди синтаксические ошибки в HCL: опечатки в ключевых словах, незакрытые блоки, неверные типы значений, невалидные атрибуты.
|
||||||
|
НЕ давай рекомендаций по улучшению, НЕ анализируй логику, НЕ комментируй архитектуру.
|
||||||
|
Формат ответа: список найденных ошибок или "✓ Синтаксических ошибок не обнаружено."`,
|
||||||
|
|
||||||
|
2: `Ты — анализатор Terraform конфигурации. Твоя задача: синтаксис + базовые проблемы.
|
||||||
|
1. Синтаксические ошибки в HCL.
|
||||||
|
2. Базовые проблемы: неиспользуемые переменные/ресурсы, пустые обязательные значения, дублирующиеся ресурсы.
|
||||||
|
НЕ анализируй зависимости, архитектуру, безопасность.
|
||||||
|
Формат: список проблем с пометкой [SYNTAX] или [BASIC], или "✓ Проблем не обнаружено."`,
|
||||||
|
|
||||||
|
3: `Ты — анализатор Terraform конфигурации. Твоя задача: синтаксис + базовые проблемы + зависимости.
|
||||||
|
1. Синтаксические ошибки.
|
||||||
|
2. Базовые проблемы (неиспользуемые ресурсы, пустые значения).
|
||||||
|
3. Анализ зависимостей: потенциальные race conditions при apply/destroy, неявные зависимости, проблемы с порядком создания ресурсов.
|
||||||
|
НЕ давай рекомендаций по безопасности или рефакторингу.
|
||||||
|
Формат: список проблем с пометкой [SYNTAX], [BASIC] или [DEPS], или "✓ Проблем не обнаружено."`,
|
||||||
|
|
||||||
|
4: `Ты — анализатор Terraform конфигурации. Твоя задача: полный анализ + безопасность.
|
||||||
|
1. Синтаксические ошибки.
|
||||||
|
2. Базовые проблемы.
|
||||||
|
3. Зависимости и порядок операций.
|
||||||
|
4. Безопасность и best practices: открытые порты (0.0.0.0/0), избыточные права доступа, незашифрованные ресурсы, жёстко заданные секреты.
|
||||||
|
Формат: список проблем с пометкой [SYNTAX], [BASIC], [DEPS] или [SECURITY], или "✓ Проблем не обнаружено."`,
|
||||||
|
|
||||||
|
5: `Ты — старший DevOps-инженер, проводишь полный аудит Terraform конфигурации.
|
||||||
|
Анализируй:
|
||||||
|
1. Синтаксические ошибки.
|
||||||
|
2. Базовые проблемы.
|
||||||
|
3. Зависимости и порядок операций.
|
||||||
|
4. Безопасность и best practices.
|
||||||
|
5. Оптимизация: возможности для рефакторинга, использование модулей, устаревшие паттерны, альтернативные подходы.
|
||||||
|
Давай конкретные рекомендации с примерами кода где уместно.
|
||||||
|
Формат: список проблем и рекомендаций с пометкой [SYNTAX], [BASIC], [DEPS], [SECURITY] или [OPT].`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// levelNames — человекочитаемые названия уровней для вывода.
|
||||||
|
var levelNames = map[int]string{
|
||||||
|
0: "OFF",
|
||||||
|
1: "SYNTAX",
|
||||||
|
2: "BASIC",
|
||||||
|
3: "MODERATE",
|
||||||
|
4: "DETAILED",
|
||||||
|
5: "FULL",
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPrompt формирует финальный промпт для отправки в LLM.
|
||||||
|
// planText — вывод terraform plan (текст или JSON).
|
||||||
|
func BuildPrompt(level int, planText string) string {
|
||||||
|
sysPrompt, ok := systemPrompts[level]
|
||||||
|
if !ok {
|
||||||
|
sysPrompt = systemPrompts[1]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s\n\nTerraform plan output:\n```\n%s\n```", sysPrompt, planText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LevelName возвращает название уровня для вывода пользователю.
|
||||||
|
func LevelName(level int) string {
|
||||||
|
name, ok := levelNames[level]
|
||||||
|
if !ok {
|
||||||
|
return "UNKNOWN"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// internal/ai/provider.go
|
||||||
|
// Дата: 2026-03-12 (обновлён 2026-03-12: добавлен Groq / OpenAI-compat)
|
||||||
|
// Интерфейс LLMProvider — абстракция над любым LLM-бэкендом.
|
||||||
|
// Реализации: Google Gemini, Groq (OpenAI-compat), облачный LLM (stub).
|
||||||
|
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// LLMProvider — единственный интерфейс для всех LLM-провайдеров.
|
||||||
|
// Менять провайдер = менять одну env-переменную AI_PROVIDER.
|
||||||
|
type LLMProvider interface {
|
||||||
|
// Analyze отправляет промпт в LLM и возвращает текстовый ответ.
|
||||||
|
Analyze(ctx context.Context, prompt string) (string, error)
|
||||||
|
// Name возвращает имя провайдера для логирования.
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config — конфигурация анализатора, читается из env-переменных.
|
||||||
|
type Config struct {
|
||||||
|
// Provider: "groq" (по умолчанию), "google", "cloud"
|
||||||
|
Provider string
|
||||||
|
// HintLevel: 0–5, если 0 — AI не вызывается
|
||||||
|
HintLevel int
|
||||||
|
// Groq / OpenAI-совместимые провайдеры
|
||||||
|
GroqAPIKey string
|
||||||
|
GroqModel string // по умолчанию "llama-3.3-70b-versatile"
|
||||||
|
GroqEndpoint string // по умолчанию https://api.groq.com/openai/v1/chat/completions
|
||||||
|
// Google Gemini (геоблок в РФ)
|
||||||
|
GoogleAPIKey string
|
||||||
|
GoogleModel string // по умолчанию "gemini-2.0-flash"
|
||||||
|
// Будущий облачный LLM
|
||||||
|
CloudEndpoint string
|
||||||
|
CloudToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProvider — фабрика провайдеров. Выбор по Config.Provider.
|
||||||
|
// При добавлении нового провайдера — только сюда добавить case.
|
||||||
|
func NewProvider(cfg Config) (LLMProvider, error) {
|
||||||
|
switch cfg.Provider {
|
||||||
|
case "groq":
|
||||||
|
return NewOpenAICompatProvider(cfg.GroqEndpoint, cfg.GroqAPIKey, cfg.GroqModel)
|
||||||
|
case "google":
|
||||||
|
model := cfg.GoogleModel
|
||||||
|
if model == "" {
|
||||||
|
model = "gemini-2.0-flash"
|
||||||
|
}
|
||||||
|
return NewGeminiProvider(cfg.GoogleAPIKey, model)
|
||||||
|
case "cloud":
|
||||||
|
return NewCloudLLMProvider(cfg.CloudEndpoint, cfg.CloudToken)
|
||||||
|
default:
|
||||||
|
// По умолчанию groq — работает из РФ без геоблоков
|
||||||
|
return NewOpenAICompatProvider(cfg.GroqEndpoint, cfg.GroqAPIKey, cfg.GroqModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,7 +117,7 @@ gpg --batch --yes --detach-sign --default-key "$key_id" \
|
|||||||
"${BUILD_DIR}/terraform-provider-sless_${VERSION}_SHA256SUMS"
|
"${BUILD_DIR}/terraform-provider-sless_${VERSION}_SHA256SUMS"
|
||||||
|
|
||||||
echo " Загружаем в S3..."
|
echo " Загружаем в S3..."
|
||||||
# Алиас mc включается в путь: <alias>/<bucket>/<path>
|
# mc = MinIO Client (/usr/local/bin/mc); Midnight Commander переименован в midn
|
||||||
mc alias set sless-registry "$s3_url" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4 >/dev/null
|
mc alias set sless-registry "$s3_url" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4 >/dev/null
|
||||||
S3_PATH="sless-registry/${S3_BUCKET}/${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}/"
|
S3_PATH="sless-registry/${S3_BUCKET}/${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}/"
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"terraform-provider-sless/internal/client"
|
"terraform-provider-sless/internal/client"
|
||||||
@@ -45,6 +46,7 @@ type SlessProviderModel struct {
|
|||||||
Endpoint types.String `tfsdk:"endpoint"`
|
Endpoint types.String `tfsdk:"endpoint"`
|
||||||
Token types.String `tfsdk:"token"`
|
Token types.String `tfsdk:"token"`
|
||||||
NubesEndpoint types.String `tfsdk:"nubes_endpoint"`
|
NubesEndpoint types.String `tfsdk:"nubes_endpoint"`
|
||||||
|
AIHintLevel types.Int64 `tfsdk:"ai_hint_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New возвращает фабрику провайдера — точная копия паттерна nubes.
|
// New возвращает фабрику провайдера — точная копия паттерна nubes.
|
||||||
@@ -75,6 +77,10 @@ func (p *SlessProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp
|
|||||||
MarkdownDescription: "URL nubes API для валидации токена (env: NUBES_ENDPOINT). Опционально. Пример: https://deck-api.ngcloud.ru/api/v1",
|
MarkdownDescription: "URL nubes API для валидации токена (env: NUBES_ENDPOINT). Опционально. Пример: https://deck-api.ngcloud.ru/api/v1",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
"ai_hint_level": schema.Int64Attribute{
|
||||||
|
MarkdownDescription: "Уровень AI-анализа terraform plan: 0=off, 1=syntax, 2=basic, 3=moderate, 4=detailed, 5=full. По умолчанию 0 (выключен).",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +166,20 @@ func (p *SlessProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 7. Экспортируем ai_hint_level в env AI_HINT_LEVEL для sless-plan ---
|
||||||
|
// sless-plan читает AI_HINT_LEVEL при запуске — так уровень из provider "sless" подхватывается автоматически.
|
||||||
|
// Если атрибут не задан — уровень 0 (AI выключен), env не выставляется.
|
||||||
|
if !config.AIHintLevel.IsNull() {
|
||||||
|
level := int(config.AIHintLevel.ValueInt64())
|
||||||
|
if level < 0 {
|
||||||
|
level = 0
|
||||||
|
}
|
||||||
|
if level > 5 {
|
||||||
|
level = 5
|
||||||
|
}
|
||||||
|
os.Setenv("AI_HINT_LEVEL", strconv.Itoa(level))
|
||||||
|
}
|
||||||
|
|
||||||
resp.ResourceData = c
|
resp.ResourceData = c
|
||||||
resp.DataSourceData = c
|
resp.DataSourceData = c
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user