Fix provider review findings
This commit is contained in:
@@ -0,0 +1,173 @@
|
|||||||
|
# Code Review провайдера — Opus — 2026-08-31
|
||||||
|
|
||||||
|
**Источник:** анализ и код-ревью через VS Code Copilot Chat
|
||||||
|
**Статус:** анализ завершён; часть исправлений внесена 2026-08-31
|
||||||
|
|
||||||
|
## Область анализа
|
||||||
|
|
||||||
|
Проверены:
|
||||||
|
|
||||||
|
- рукописное ядро провайдера в `provider/internal/core` и `provider/internal/resources_core`;
|
||||||
|
- CRUD, state management и валидация;
|
||||||
|
- HTTP-слой и `client.go`;
|
||||||
|
- регистрация провайдера и TLS-настройки;
|
||||||
|
- генераторы Go-ресурсов, YAML и build-пайплайн;
|
||||||
|
- Python- и shell-скрипты;
|
||||||
|
- gateway.
|
||||||
|
|
||||||
|
## Критичные находки
|
||||||
|
|
||||||
|
### 1. Отладочный лог с данными инстансов пишется в `/tmp` безусловно
|
||||||
|
|
||||||
|
В `provider/internal/core/client.go:629-637` замыкание `debug()` в `FindInstanceByDisplayName` всегда пишет в `/tmp/nubes_find_debug.log` с правами `0644`. В лог попадают `instanceUid`, `displayName` и `serviceId`.
|
||||||
|
|
||||||
|
Файл не защищён условием `NUBES_DEBUG_HTTP`, не ротируется и не очищается. Это создаёт риск раскрытия данных и неконтролируемого роста файла.
|
||||||
|
|
||||||
|
**Рекомендация:** убрать постоянную запись либо включать её только через явный debug-флаг; использовать безопасный путь и контролируемую ротацию.
|
||||||
|
|
||||||
|
### 2. Bearer-токен попадает в stderr при HTTP-отладке
|
||||||
|
|
||||||
|
В `provider/internal/core/client.go:1100-1101` вызов `httputil.DumpRequestOut(req, ...)` выводит полный исходящий запрос вместе с заголовком `Authorization: Bearer <token>` при `NUBES_DEBUG_HTTP=1`.
|
||||||
|
|
||||||
|
Токен может попасть в логи CI/CD или окружения выполнения.
|
||||||
|
|
||||||
|
**Рекомендация:** перед дампом удалять или маскировать `Authorization`; не выводить секреты ни в одном режиме.
|
||||||
|
|
||||||
|
### 3. В Python-скрипте сетевые вызовы выполняются без таймаутов
|
||||||
|
|
||||||
|
В `scripts/check_cloud_instances.py:87-88` вызовы `self.session.get(...)` не передают `timeout=`. При зависании API процесс может ожидать ответ бесконечно.
|
||||||
|
|
||||||
|
**Рекомендация:** добавить явные таймауты ко всем HTTP-вызовам и определить единое значение или конфигурационный параметр.
|
||||||
|
|
||||||
|
## Существенные находки
|
||||||
|
|
||||||
|
### 4. Retry сетевых ошибок применяется к POST-запросам
|
||||||
|
|
||||||
|
В `provider/internal/core/client.go:1113-1120` при сетевой ошибке повторяется любой HTTP-метод, включая POST к `/instances` и `/instanceOperations`.
|
||||||
|
|
||||||
|
Если сервер принял запрос, но ответ потерян, повтор может создать дубликат инстанса или операции. Идемпотентность POST не гарантирована.
|
||||||
|
|
||||||
|
**Рекомендация:** ограничить retry идемпотентными методами либо использовать идемпотency key и явную серверную поддержку повторов.
|
||||||
|
|
||||||
|
### 5. Ответ `401 Unauthorized` включён в retryable
|
||||||
|
|
||||||
|
В `provider/internal/core/client.go:1150-1156` статус `401` считается повторяемым. Протухший или неверный токен приводит к трём попыткам с задержкой, маскируя исходную ошибку авторизации и увеличивая время отказа.
|
||||||
|
|
||||||
|
**Рекомендация:** исключить `401` из retryable; возвращать ошибку авторизации сразу.
|
||||||
|
|
||||||
|
### 6. Gateway раскрывает внутренние upstream-адреса
|
||||||
|
|
||||||
|
В `gateway/server.js:60-71` корневой endpoint `/` и обработчик 404 возвращают наружу адреса `upstream` для маршрутов.
|
||||||
|
|
||||||
|
Публичный ответ раскрывает внутреннюю топологию сервисов.
|
||||||
|
|
||||||
|
**Рекомендация:** убрать `upstream` из публичных ответов; внутренние адреса оставлять только в серверных логах с необходимой санацией.
|
||||||
|
|
||||||
|
### 7. Некорректное определение неуспешной операции в Python
|
||||||
|
|
||||||
|
В `scripts/check_cloud_instances.py:187-189` используется сравнение `last_op.get("isSuccessful") == False`. При отсутствии поля возвращается `None`, поэтому состояние `OPERATION_FAILED` не определяется.
|
||||||
|
|
||||||
|
**Рекомендация:** использовать проверку `is False` либо явно обрабатывать отсутствие ключа согласно контракту API.
|
||||||
|
|
||||||
|
## Умеренные находки
|
||||||
|
|
||||||
|
### 8. Retry-логика дублируется в трёх местах
|
||||||
|
|
||||||
|
В `provider/internal/core/client.go:777-905` похожие циклы retry присутствуют в `doRequest`, `GetInstanceState` и `GetInstanceStateRaw`.
|
||||||
|
|
||||||
|
Дублирование увеличивает риск расхождения поведения и повторного появления ошибок безопасности.
|
||||||
|
|
||||||
|
**Рекомендация:** вынести общую retry-логику в единый внутренний helper с параметрами метода, таймаутов и политики повторов.
|
||||||
|
|
||||||
|
### 9. Пагинация имеет тихий предел 10 000 инстансов
|
||||||
|
|
||||||
|
В fallback-ветке `FindInstanceByDisplayName` (`provider/internal/core/client.go:747-749`) поиск прекращается после `page > 100` при размере страницы `100`.
|
||||||
|
|
||||||
|
При большем количестве инстансов совпадение может не быть найдено без предупреждения.
|
||||||
|
|
||||||
|
**Рекомендация:** убрать произвольный предел либо возвращать диагностируемую ошибку/предупреждение при достижении лимита.
|
||||||
|
|
||||||
|
### 10. Ошибка `gofmt` не останавливает генерацию
|
||||||
|
|
||||||
|
`FormatSourceOrWarn` в `TOOLS/resource-generator/writers.go:61` при ошибке форматирования только выводит предупреждение и записывает исходник.
|
||||||
|
|
||||||
|
В результате pipeline может сохранить неформатированный или потенциально некомпилируемый Go-код.
|
||||||
|
|
||||||
|
**Рекомендация:** считать ошибку форматирования фатальной для генерации либо выполнять последующую обязательную компиляционную проверку.
|
||||||
|
|
||||||
|
### 11. Секрет передаётся в командной строке shell-скрипта
|
||||||
|
|
||||||
|
В `TOOLS/s3_notification_example.sh:74` значение `SECRET_KEY` передаётся аргументом в `mc alias set`.
|
||||||
|
|
||||||
|
Секрет может быть виден через `ps` или аналогичный список процессов.
|
||||||
|
|
||||||
|
**Рекомендация:** использовать механизм передачи секрета через stdin, переменную окружения, конфигурационный файл с безопасными правами или другой поддерживаемый секретный канал.
|
||||||
|
|
||||||
|
## Дополнительные замечания
|
||||||
|
|
||||||
|
- В `provider/internal/core/client.go` ссылка на `tools/gen_v2/generate_resources_v2.go` обновлена на актуальный путь `TOOLS/resource-generator/internal/templates/instance.go`.
|
||||||
|
- В исходниках генератора (`TOOLS/resource-generator/internal/templates/*`, `TOOLS/resource-generator/internal/writers/writers.go`) метка `Code generated by tools/gen_v2` обновлена на `Code generated by TOOLS/resource-generator`.
|
||||||
|
- Текущий `provider/internal/resources_gen/registry.go` обновлён на новую метку генератора.
|
||||||
|
- `TOOLS/resource-generator/main.go` переведён на `run()` с корректным `exit code=1` и агрегированным отчётом по ошибкам записи ресурсов (instance/subresource/action).
|
||||||
|
- Пути debug-логов в `provider/internal/core/client.go` переведены на `os.TempDir()` с override через `NUBES_DEBUG_DIR` (без хардкода `/tmp`).
|
||||||
|
|
||||||
|
## Что выглядит хорошо
|
||||||
|
|
||||||
|
- Сериализация операций на инстансе через `instanceMutexes` в `client.go` защищает от параллельных операций API.
|
||||||
|
- TLS настроен с `MinVersion: TLS 1.2`; `InsecureSkipVerify` по умолчанию равен `false`.
|
||||||
|
- `api_token` отмечен как `Sensitive: true` в схеме провайдера.
|
||||||
|
- Канонизация JSON для сравнения state устраняет ложные различия из-за порядка ключей.
|
||||||
|
|
||||||
|
## Итоговый статус
|
||||||
|
|
||||||
|
| Находка | Статус |
|
||||||
|
|---|---|
|
||||||
|
| Безусловная запись данных инстансов в `/tmp` | Исправлено: debug gated + права `0600` |
|
||||||
|
| Bearer-токен в HTTP debug dump | Исправлено: `Authorization` маскируется |
|
||||||
|
| Python HTTP-вызовы без таймаутов | Исправлено: добавлен `REQUEST_TIMEOUT` |
|
||||||
|
| Retry POST-запросов | Исправлено: retry сетевых ошибок только для GET |
|
||||||
|
| `401` в retryable | Исправлено: исключён из retryable |
|
||||||
|
| Раскрытие upstream в gateway | Исправлено: `upstream` удалён из root-ответа |
|
||||||
|
| Ошибка определения `OPERATION_FAILED` | Исправлено: сравнение через `is False` |
|
||||||
|
| Дублирование retry-логики | Исправлено: общий helper для чтения состояния |
|
||||||
|
| Тихий предел пагинации | Частично исправлено: добавлена явная ошибка при достижении лимита |
|
||||||
|
| Некритичная ошибка `gofmt` в генераторе | Исправлено: fail-fast при ошибке форматирования |
|
||||||
|
| Секрет в аргументах shell-команды | Исправлено: исключена передача в argv |
|
||||||
|
|
||||||
|
## Выполненные изменения (2026-08-31)
|
||||||
|
|
||||||
|
- `provider/internal/core/client.go`:
|
||||||
|
- debug-лог `FindInstanceByDisplayName` теперь пишется только при `NUBES_DEBUG_HTTP=1`;
|
||||||
|
- права debug-логов снижены до `0600`;
|
||||||
|
- в stderr-дампе HTTP-запроса маскируется заголовок `Authorization`;
|
||||||
|
- retry сетевых ошибок ограничен методом `GET`;
|
||||||
|
- `401 Unauthorized` удалён из `isRetryable`;
|
||||||
|
- при достижении лимита fallback-пагинации возвращается явная ошибка.
|
||||||
|
- `GetInstanceState` и `GetInstanceStateRaw` переведены на общий helper `getInstanceStateWithRetry` с единым retry/HTTP-поведением.
|
||||||
|
- `scripts/check_cloud_instances.py`:
|
||||||
|
- добавлен `REQUEST_TIMEOUT = 30` и применён ко всем `session.get(...)`;
|
||||||
|
- проверка failed-операции изменена на `is False`.
|
||||||
|
- `gateway/server.js`:
|
||||||
|
- удалено поле `upstream` из публичного ответа `GET /`.
|
||||||
|
- `TOOLS/resource-generator/internal/helpers/helpers.go`:
|
||||||
|
- `FormatSourceOrWarn` переведён на fail-fast: возвращает ошибку при сбое `gofmt`.
|
||||||
|
- `TOOLS/resource-generator/internal/writers/writers.go`:
|
||||||
|
- все вызовы форматирования обрабатывают ошибку и прерывают генерацию.
|
||||||
|
- `TOOLS/resource-generator/main.go`:
|
||||||
|
- убраны `panic` на первом сбое записи ресурса;
|
||||||
|
- добавлена агрегация ошибок генерации с отчётом по каждому ресурсу;
|
||||||
|
- завершение с `exit code=1` и человекочитаемым сообщением в stderr.
|
||||||
|
- `TOOLS/resource-generator/internal/templates/instance.go`:
|
||||||
|
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
|
||||||
|
- `TOOLS/resource-generator/internal/templates/subresource.go`:
|
||||||
|
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
|
||||||
|
- `TOOLS/resource-generator/internal/templates/action.go`:
|
||||||
|
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
|
||||||
|
- `provider/internal/resources_gen/registry.go`:
|
||||||
|
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
|
||||||
|
- `scripts/s3_notification_example.sh`:
|
||||||
|
- убрана передача секрета в аргументах процесса;
|
||||||
|
- для `mc` используется временный `--config-dir` и переменная `MC_HOST_<alias>`.
|
||||||
|
- `provider/internal/core/client.go`:
|
||||||
|
- debug log path переведён на `os.TempDir()`;
|
||||||
|
- добавлен override директории через `NUBES_DEBUG_DIR`.
|
||||||
@@ -4,7 +4,7 @@ TOKEN_FILE="secrets/dev.token"
|
|||||||
|
|
||||||
# Release versions
|
# Release versions
|
||||||
# Version
|
# Version
|
||||||
VERSION="3.0.7"
|
VERSION="3.0.8"
|
||||||
|
|
||||||
NAMESPACE="nubes-dev"
|
NAMESPACE="nubes-dev"
|
||||||
PROVIDER_NAME="nubes"
|
PROVIDER_NAME="nubes"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ TOKEN_FILE="secrets/prod.token"
|
|||||||
|
|
||||||
# Release versions
|
# Release versions
|
||||||
# Version
|
# Version
|
||||||
VERSION="2.0.7"
|
VERSION="2.0.8"
|
||||||
|
|
||||||
NAMESPACE="nubes"
|
NAMESPACE="nubes"
|
||||||
PROVIDER_NAME="nubes"
|
PROVIDER_NAME="nubes"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ NUBES_API_ENDPOINT="https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
|
|||||||
TOKEN_FILE="secrets/test.token"
|
TOKEN_FILE="secrets/test.token"
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
VERSION="5.0.7"
|
VERSION="5.0.8"
|
||||||
|
|
||||||
NAMESPACE="nubes-test"
|
NAMESPACE="nubes-test"
|
||||||
PROVIDER_NAME="nubes"
|
PROVIDER_NAME="nubes"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package helpers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -168,14 +167,13 @@ func EnsureGoIdent(s string) string {
|
|||||||
return "R" + s
|
return "R" + s
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatSourceOrWarn форматирует Go-код. При ошибке пишет warning в stderr.
|
// FormatSourceOrWarn форматирует Go-код и возвращает ошибку при сбое gofmt.
|
||||||
func FormatSourceOrWarn(filePath string, src []byte) []byte {
|
func FormatSourceOrWarn(filePath string, src []byte) ([]byte, error) {
|
||||||
formatted, err := format.Source(src)
|
formatted, err := format.Source(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARNING: gofmt failed for %s: %v — writing unformatted code\n", filePath, err)
|
return nil, fmt.Errorf("gofmt failed for %s: %w", filePath, err)
|
||||||
return src
|
|
||||||
}
|
}
|
||||||
return formatted
|
return formatted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanModifierType возвращает суффикс типа план-модификатора (Bool/Int64/String).
|
// PlanModifierType возвращает суффикс типа план-модификатора (Bool/Int64/String).
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Code generated by tools/gen_v2. DO NOT EDIT.
|
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||||||
// Service: {{.ServiceName}}
|
// Service: {{.ServiceName}}
|
||||||
// Action: {{.ActionName}}
|
// Action: {{.ActionName}}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Code generated by tools/gen_v2. DO NOT EDIT.
|
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||||||
// Service: {{.Name}}
|
// Service: {{.Name}}
|
||||||
// Service ID: {{.ServiceID}}
|
// Service ID: {{.ServiceID}}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Code generated by tools/gen_v2. DO NOT EDIT.
|
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||||||
// Service: {{.ServiceName}}
|
// Service: {{.ServiceName}}
|
||||||
// Subresource: {{.SubName}}
|
// Subresource: {{.SubName}}
|
||||||
|
|
||||||
@@ -439,4 +439,3 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Configure(_
|
|||||||
r.client = client
|
r.client = client
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ func WriteInstanceResource(outDir string, svc types.GenResource) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
formatted, err := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return os.WriteFile(filePath, formatted, 0644)
|
return os.WriteFile(filePath, formatted, 0644)
|
||||||
}
|
}
|
||||||
@@ -91,7 +94,10 @@ func WriteSubresource(outDir string, sr types.GenSubresource) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
formatted, err := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return os.WriteFile(filePath, formatted, 0644)
|
return os.WriteFile(filePath, formatted, 0644)
|
||||||
}
|
}
|
||||||
@@ -121,7 +127,10 @@ func WriteActionResource(outDir string, act types.GenAction) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
formatted, err := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return os.WriteFile(filePath, formatted, 0644)
|
return os.WriteFile(filePath, formatted, 0644)
|
||||||
}
|
}
|
||||||
@@ -131,7 +140,7 @@ func WriteRegistry(outDir string, services []types.GenResource, subs []types.Gen
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
buf.WriteString("package resources_gen\n\n")
|
buf.WriteString("package resources_gen\n\n")
|
||||||
buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n")
|
buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n")
|
||||||
buf.WriteString("// Code generated by tools/gen_v2. DO NOT EDIT.\n")
|
buf.WriteString("// Code generated by TOOLS/resource-generator. DO NOT EDIT.\n")
|
||||||
buf.WriteString("func AllResources() []func() resource.Resource {\n")
|
buf.WriteString("func AllResources() []func() resource.Resource {\n")
|
||||||
buf.WriteString("\treturn []func() resource.Resource{\n")
|
buf.WriteString("\treturn []func() resource.Resource{\n")
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
@@ -147,7 +156,10 @@ func WriteRegistry(outDir string, services []types.GenResource, subs []types.Gen
|
|||||||
buf.WriteString("}\n")
|
buf.WriteString("}\n")
|
||||||
|
|
||||||
regPath := filepath.Join(outDir, "registry.go")
|
regPath := filepath.Join(outDir, "registry.go")
|
||||||
formatted := helpers.FormatSourceOrWarn(regPath, buf.Bytes())
|
formatted, err := helpers.FormatSourceOrWarn(regPath, buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return os.WriteFile(regPath, formatted, 0644)
|
return os.WriteFile(regPath, formatted, 0644)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -23,37 +25,66 @@ import (
|
|||||||
"resource-generator/internal/writers"
|
"resource-generator/internal/writers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type serviceWriteErr struct {
|
||||||
|
kind string
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
resourcesDir := strings.TrimSpace(os.Getenv("NUBES_RESOURCES_DIR"))
|
resourcesDir := strings.TrimSpace(os.Getenv("NUBES_RESOURCES_DIR"))
|
||||||
if resourcesDir == "" {
|
if resourcesDir == "" {
|
||||||
panic("NUBES_RESOURCES_DIR is required — must point to generated/{stand}/resources_yaml/")
|
return fmt.Errorf("NUBES_RESOURCES_DIR is required — must point to generated/{stand}/resources_yaml/")
|
||||||
}
|
}
|
||||||
outDir := strings.TrimSpace(os.Getenv("NUBES_RESOURCES_GEN_DIR"))
|
outDir := strings.TrimSpace(os.Getenv("NUBES_RESOURCES_GEN_DIR"))
|
||||||
if outDir == "" {
|
if outDir == "" {
|
||||||
panic("NUBES_RESOURCES_GEN_DIR is required — must point to generated/{stand}/go/")
|
return fmt.Errorf("NUBES_RESOURCES_GEN_DIR is required — must point to generated/{stand}/go/")
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceResources, subresources, actions, err := loader.LoadSpecs(resourcesDir)
|
instanceResources, subresources, actions, err := loader.LoadSpecs(resourcesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var writeErrs []serviceWriteErr
|
||||||
|
|
||||||
for _, svc := range instanceResources {
|
for _, svc := range instanceResources {
|
||||||
if err := writers.WriteInstanceResource(outDir, svc); err != nil {
|
if err := writers.WriteInstanceResource(outDir, svc); err != nil {
|
||||||
panic(err)
|
writeErrs = append(writeErrs, serviceWriteErr{kind: "instance", name: svc.Name, err: err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, sr := range subresources {
|
for _, sr := range subresources {
|
||||||
if err := writers.WriteSubresource(outDir, sr); err != nil {
|
if err := writers.WriteSubresource(outDir, sr); err != nil {
|
||||||
panic(err)
|
name := fmt.Sprintf("%s_%s", sr.ServiceName, sr.SubName)
|
||||||
|
writeErrs = append(writeErrs, serviceWriteErr{kind: "subresource", name: name, err: err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, act := range actions {
|
for _, act := range actions {
|
||||||
if err := writers.WriteActionResource(outDir, act); err != nil {
|
if err := writers.WriteActionResource(outDir, act); err != nil {
|
||||||
panic(err)
|
name := fmt.Sprintf("%s_%s", act.ServiceName, act.ActionName)
|
||||||
|
writeErrs = append(writeErrs, serviceWriteErr{kind: "action", name: name, err: err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(writeErrs) > 0 {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("generation failed for one or more resources:\n")
|
||||||
|
for _, we := range writeErrs {
|
||||||
|
b.WriteString(fmt.Sprintf("- %s %s: %v\n", we.kind, we.name, we.err))
|
||||||
|
}
|
||||||
|
return errors.New(strings.TrimSpace(b.String()))
|
||||||
|
}
|
||||||
|
|
||||||
if err := writers.WriteRegistry(outDir, instanceResources, subresources, actions); err != nil {
|
if err := writers.WriteRegistry(outDir, instanceResources, subresources, actions); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -90,6 +91,25 @@ var ctxKeyLogLevel = ctxKeyLogLevelType{}
|
|||||||
// Go-http-client по умолчанию блокируется фильтром ddos-guard.
|
// Go-http-client по умолчанию блокируется фильтром ddos-guard.
|
||||||
const userAgent = "Mozilla/5.0"
|
const userAgent = "Mozilla/5.0"
|
||||||
|
|
||||||
|
func debugLogPath(fileName string) string {
|
||||||
|
if dir := strings.TrimSpace(os.Getenv("NUBES_DEBUG_DIR")); dir != "" {
|
||||||
|
return filepath.Join(dir, fileName)
|
||||||
|
}
|
||||||
|
return filepath.Join(os.TempDir(), fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHTTPDebugEnabled() bool {
|
||||||
|
return os.Getenv("NUBES_DEBUG_HTTP") == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeAuthHeader(h http.Header) http.Header {
|
||||||
|
cloned := h.Clone()
|
||||||
|
if cloned.Get("Authorization") != "" {
|
||||||
|
cloned.Set("Authorization", "Bearer [REDACTED]")
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
// CtxWithLogLevel возвращает ctx с переопределённым уровнем логирования.
|
// CtxWithLogLevel возвращает ctx с переопределённым уровнем логирования.
|
||||||
func CtxWithLogLevel(ctx context.Context, level string) context.Context {
|
func CtxWithLogLevel(ctx context.Context, level string) context.Context {
|
||||||
return context.WithValue(ctx, ctxKeyLogLevel, level)
|
return context.WithValue(ctx, ctxKeyLogLevel, level)
|
||||||
@@ -621,7 +641,10 @@ type InstanceStateResponse struct {
|
|||||||
// Если найдено больше одного non-deleted инстанса — возвращает ошибку.
|
// Если найдено больше одного non-deleted инстанса — возвращает ошибку.
|
||||||
func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, serviceId int, displayName string) (*InstanceStateResponse, error) {
|
func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, serviceId int, displayName string) (*InstanceStateResponse, error) {
|
||||||
debug := func(format string, args ...interface{}) {
|
debug := func(format string, args ...interface{}) {
|
||||||
f, err := os.OpenFile("/tmp/nubes_find_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
if !isHTTPDebugEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(debugLogPath("nubes_find_debug.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -684,6 +707,8 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service
|
|||||||
// Если быстрый поиск не дал результатов — пагинированный fallback
|
// Если быстрый поиск не дал результатов — пагинированный fallback
|
||||||
if len(found) == 0 {
|
if len(found) == 0 {
|
||||||
debug("[FIND-DEBUG] search path failed, entering fallback (serviceId=%d, name=%q)", serviceId, displayName)
|
debug("[FIND-DEBUG] search path failed, entering fallback (serviceId=%d, name=%q)", serviceId, displayName)
|
||||||
|
const maxFallbackPages = 100
|
||||||
|
hitPageLimit := false
|
||||||
page := 1
|
page := 1
|
||||||
for {
|
for {
|
||||||
reqURL := c.buildURL(fmt.Sprintf("/instances?page=%d&size=100", page))
|
reqURL := c.buildURL(fmt.Sprintf("/instances?page=%d&size=100", page))
|
||||||
@@ -753,10 +778,15 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service
|
|||||||
}
|
}
|
||||||
|
|
||||||
page++
|
page++
|
||||||
if page > 100 {
|
if page > maxFallbackPages {
|
||||||
|
hitPageLimit = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hitPageLimit && len(found) == 0 {
|
||||||
|
return nil, fmt.Errorf("поиск инстанса по display_name достиг лимита пагинации (%d страниц); сузьте фильтр или повторите с более точными параметрами", maxFallbackPages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(found) == 0 {
|
if len(found) == 0 {
|
||||||
@@ -778,58 +808,14 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *UniversalClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
func (c *UniversalClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
||||||
const maxRetries = 3
|
state, err := c.getInstanceStateWithRetry(ctx, instanceUid)
|
||||||
baseDelay := 2 * time.Second
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
||||||
if attempt > 0 {
|
|
||||||
select {
|
|
||||||
case <-time.After(baseDelay * time.Duration(1<<(attempt-1))):
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
url := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid))
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", userAgent)
|
if err := validateInstanceStatus(state); err != nil {
|
||||||
if c.ApiToken != "" {
|
|
||||||
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
lastErr = fmt.Errorf("HTTP статус %d", resp.StatusCode)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("HTTP статус %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res struct {
|
|
||||||
Instance InstanceStateResponse `json:"instance"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return state, nil
|
||||||
if err := validateInstanceStatus(&res.Instance); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res.Instance, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("GetInstanceState failed after %d retries: %w", maxRetries, lastErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isInstanceDeleted(state *InstanceStateResponse) bool {
|
func isInstanceDeleted(state *InstanceStateResponse) bool {
|
||||||
@@ -845,55 +831,24 @@ func isInstanceDeleted(state *InstanceStateResponse) bool {
|
|||||||
// GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса.
|
// GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса.
|
||||||
// Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы.
|
// Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы.
|
||||||
func (c *UniversalClient) GetInstanceStateRaw(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
func (c *UniversalClient) GetInstanceStateRaw(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
||||||
const maxRetries = 3
|
return c.getInstanceStateWithRetry(ctx, instanceUid)
|
||||||
baseDelay := 2 * time.Second
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
||||||
if attempt > 0 {
|
|
||||||
select {
|
|
||||||
case <-time.After(baseDelay * time.Duration(1<<(attempt-1))):
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reqURL := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid))
|
func (c *UniversalClient) getInstanceStateWithRetry(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instances/%s", instanceUid), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", userAgent)
|
|
||||||
if c.ApiToken != "" {
|
|
||||||
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
lastErr = fmt.Errorf("HTTP статус %d", resp.StatusCode)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("HTTP статус %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res struct {
|
var res struct {
|
||||||
Instance InstanceStateResponse `json:"instance"`
|
Instance InstanceStateResponse `json:"instance"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res.Instance, nil
|
return &res.Instance, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("GetInstanceStateRaw failed after %d retries: %w", maxRetries, lastErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== UNIVERSAL OPERATION WAIT (APPEND-ONLY) =====
|
// ===== UNIVERSAL OPERATION WAIT (APPEND-ONLY) =====
|
||||||
//
|
//
|
||||||
@@ -1096,14 +1051,16 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG
|
// DEBUG
|
||||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
if isHTTPDebugEnabled() {
|
||||||
dump, _ := httputil.DumpRequestOut(req, body != nil)
|
reqForDump := req.Clone(req.Context())
|
||||||
|
reqForDump.Header = sanitizeAuthHeader(req.Header)
|
||||||
|
dump, _ := httputil.DumpRequestOut(reqForDump, body != nil)
|
||||||
fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, path, dump)
|
fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, path, dump)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG в файл
|
// DEBUG в файл
|
||||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
if isHTTPDebugEnabled() {
|
||||||
f, _ := os.OpenFile("/tmp/nubes_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
f, _ := os.OpenFile(debugLogPath("nubes_debug.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
fmt.Fprintf(f, ">>> %s %s\n", method, req.URL.String())
|
fmt.Fprintf(f, ">>> %s %s\n", method, req.URL.String())
|
||||||
f.Close()
|
f.Close()
|
||||||
@@ -1112,8 +1069,8 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa
|
|||||||
resp, err := c.HttpClient.Do(req)
|
resp, err := c.HttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
// Сетевые ошибки — retry (кроме последней попытки)
|
// Сетевые ошибки: retry только для идемпотентного GET.
|
||||||
if attempt < maxRetries {
|
if attempt < maxRetries && method == "GET" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -1149,8 +1106,7 @@ func isRetryable(statusCode int) bool {
|
|||||||
return statusCode == http.StatusTooManyRequests || // 429
|
return statusCode == http.StatusTooManyRequests || // 429
|
||||||
statusCode == http.StatusServiceUnavailable || // 503
|
statusCode == http.StatusServiceUnavailable || // 503
|
||||||
statusCode == http.StatusBadGateway || // 502
|
statusCode == http.StatusBadGateway || // 502
|
||||||
statusCode == http.StatusGatewayTimeout || // 504
|
statusCode == http.StatusGatewayTimeout // 504
|
||||||
statusCode == http.StatusUnauthorized // 401 — Gateway иногда отбрасывает валидный JWT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
|
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
|
||||||
@@ -1197,7 +1153,7 @@ func extractUIDFromLocation(loc string) string {
|
|||||||
// Однако в state UUID должен сохраняться в том регистре, который написал пользователь
|
// Однако в state UUID должен сохраняться в том регистре, который написал пользователь
|
||||||
// (иначе plan != state → "Provider produced inconsistent result after apply").
|
// (иначе plan != state → "Provider produced inconsistent result after apply").
|
||||||
// Восстановление регистра делается в Create/Read/Update generated resource, см. шаблон
|
// Восстановление регистра делается в Create/Read/Update generated resource, см. шаблон
|
||||||
// instanceTemplate в tools/gen_v2/generate_resources_v2.go (блоки "Restore user-provided casing").
|
// instanceTemplate в TOOLS/resource-generator/internal/templates/instance.go (блоки "Restore user-provided casing").
|
||||||
|
|
||||||
func (c *UniversalClient) resolveRefSvcParamValues(ctx context.Context, opParams []universalCfsParam, params map[int]string) (map[int]string, error) {
|
func (c *UniversalClient) resolveRefSvcParamValues(ctx context.Context, opParams []universalCfsParam, params map[int]string) (map[int]string, error) {
|
||||||
if len(params) == 0 || len(opParams) == 0 {
|
if len(params) == 0 || len(opParams) == 0 {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class CloudInstanceChecker:
|
|||||||
|
|
||||||
# ⛔ LEGACY: deck-api ЗАКРЫВАЕТСЯ. Использовать lk-api-gateway.
|
# ⛔ LEGACY: deck-api ЗАКРЫВАЕТСЯ. Использовать lk-api-gateway.
|
||||||
BASE_URL = "https://lk-api-gateway.ngcloud.ru/api/v1/svc"
|
BASE_URL = "https://lk-api-gateway.ngcloud.ru/api/v1/svc"
|
||||||
|
REQUEST_TIMEOUT = 30
|
||||||
|
|
||||||
def __init__(self, token: str):
|
def __init__(self, token: str):
|
||||||
"""
|
"""
|
||||||
@@ -88,7 +89,11 @@ class CloudInstanceChecker:
|
|||||||
"pageSize": "100"
|
"pageSize": "100"
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.session.get(f"{self.BASE_URL}/instances", params=params)
|
response = self.session.get(
|
||||||
|
f"{self.BASE_URL}/instances",
|
||||||
|
params=params,
|
||||||
|
timeout=self.REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -120,7 +125,11 @@ class CloudInstanceChecker:
|
|||||||
"fields": "instanceConfigDtCreated,instanceUid,displayName,descr,svc,state,operations,availableOperations,uptime,isDeleted,updaterLogin,updaterShortname,explainedStatus,man,dependencies,dependentInstances,svcExtendedName"
|
"fields": "instanceConfigDtCreated,instanceUid,displayName,descr,svc,state,operations,availableOperations,uptime,isDeleted,updaterLogin,updaterShortname,explainedStatus,man,dependencies,dependentInstances,svcExtendedName"
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.session.get(f"{self.BASE_URL}/instances/{instance_uid}", params=params)
|
response = self.session.get(
|
||||||
|
f"{self.BASE_URL}/instances/{instance_uid}",
|
||||||
|
params=params,
|
||||||
|
timeout=self.REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
return response.json().get("instance")
|
return response.json().get("instance")
|
||||||
@@ -184,7 +193,7 @@ class CloudInstanceChecker:
|
|||||||
operations = instance_data.get("operations", [])
|
operations = instance_data.get("operations", [])
|
||||||
if operations:
|
if operations:
|
||||||
last_op = operations[-1]
|
last_op = operations[-1]
|
||||||
if last_op.get("isSuccessful") == False:
|
if last_op.get("isSuccessful") is False:
|
||||||
return InstanceState.OPERATION_FAILED
|
return InstanceState.OPERATION_FAILED
|
||||||
|
|
||||||
return InstanceState.UNKNOWN
|
return InstanceState.UNKNOWN
|
||||||
|
|||||||
@@ -63,10 +63,18 @@ if [[ -z "$ACCESS_KEY" || -z "$SECRET_KEY" || -z "$HOST_BASE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mc alias rm "$ALIAS_NAME" >/dev/null 2>&1 || true
|
mc alias rm "$ALIAS_NAME" >/dev/null 2>&1 || true
|
||||||
mc alias set "$ALIAS_NAME" "https://$HOST_BASE" "$ACCESS_KEY" "$SECRET_KEY" >/dev/null
|
|
||||||
|
MC_CONFIG_DIR="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$MC_CONFIG_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
export MC_HOST_${ALIAS_NAME}="https://${ACCESS_KEY}:${SECRET_KEY}@${HOST_BASE}"
|
||||||
|
mc --config-dir "$MC_CONFIG_DIR" alias ls >/dev/null
|
||||||
|
|
||||||
echo "[1/3] Текущие notification-правила в бакете $BUCKET:"
|
echo "[1/3] Текущие notification-правила в бакете $BUCKET:"
|
||||||
mc event ls "$ALIAS_NAME/$BUCKET" || true
|
mc --config-dir "$MC_CONFIG_DIR" event ls "$ALIAS_NAME/$BUCKET" || true
|
||||||
|
|
||||||
echo "[2/3] Добавление правила: events=$EVENTS target=$TARGET_ARN"
|
echo "[2/3] Добавление правила: events=$EVENTS target=$TARGET_ARN"
|
||||||
EVENT_FLAGS=()
|
EVENT_FLAGS=()
|
||||||
@@ -83,9 +91,9 @@ if [[ -n "$SUFFIX" ]]; then
|
|||||||
FILTER_FLAGS+=("--suffix" "$SUFFIX")
|
FILTER_FLAGS+=("--suffix" "$SUFFIX")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mc event add "$ALIAS_NAME/$BUCKET" "$TARGET_ARN" "${EVENT_FLAGS[@]}" "${FILTER_FLAGS[@]}"
|
mc --config-dir "$MC_CONFIG_DIR" event add "$ALIAS_NAME/$BUCKET" "$TARGET_ARN" "${EVENT_FLAGS[@]}" "${FILTER_FLAGS[@]}"
|
||||||
|
|
||||||
echo "[3/3] Проверка после добавления:"
|
echo "[3/3] Проверка после добавления:"
|
||||||
mc event ls "$ALIAS_NAME/$BUCKET"
|
mc --config-dir "$MC_CONFIG_DIR" event ls "$ALIAS_NAME/$BUCKET"
|
||||||
|
|
||||||
echo "OK: правило уведомлений настроено"
|
echo "OK: правило уведомлений настроено"
|
||||||
|
|||||||
Reference in New Issue
Block a user