diff --git a/CLAUDE.md b/CLAUDE.md index f7529da2fb..8c1eb952e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,15 +2,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Important: Read PROJECT_CONTEXT.md First +## Important: Read These Documents First -**Before working on this codebase, read [PROJECT_CONTEXT.md](PROJECT_CONTEXT.md)** for: -- Project overview and directory structure -- Local development setup (Docker and non-Docker) -- Service URLs, ports, and deployment information -- CI/CD pipeline and infrastructure requirements +**Before working on this codebase, read these documents in order:** -This file (CLAUDE.md) contains **additional** architecture patterns, coding conventions, and development practices specific to this codebase that are not covered in PROJECT_CONTEXT.md. +1. **[PROJECT_CONTEXT.md](PROJECT_CONTEXT.md)** - Project overview, setup, deployment + - Directory structure and service architecture + - Local development setup (Docker and non-Docker) + - Service URLs, ports, and deployment information + - CI/CD pipeline and infrastructure requirements + +2. **[docs/CLI_OBJECT_LIFECYCLE.md](docs/CLI_OBJECT_LIFECYCLE.md)** - **CRITICAL for CLI work** + - How objects (configs, rows, notifications) are created, loaded, and saved + - State vs Manifest distinction + - Registry.Set() rules (ONLY for new objects!) + - Mapper timing and execution order + - Pull/Push operation flows + - Common pitfalls and how to avoid them + +**When to read CLI_OBJECT_LIFECYCLE.md:** +- Working on mappers (`internal/pkg/mapper/*/`) +- Working on state management (`internal/pkg/state/`) +- Adding new object types (configs, rows, notifications, etc.) +- Debugging "already exists" errors +- Debugging validation errors during pull/push + +This file (CLAUDE.md) contains **additional** architecture patterns, coding conventions, and development practices specific to this codebase that are not covered in the documents above. ## Architecture Patterns @@ -76,14 +93,23 @@ task tests ### Testing (Run Specific Tests) -**Run specific test** (recommended approach for local development): +**CRITICAL: E2E tests MUST be run in Docker Compose dev container** + +E2E tests require a complete environment with etcd, dependencies, and proper tooling. Always run them inside the Docker Compose dev container: + +```bash +# Start dev container +docker compose run --rm -u "$UID:$GID" --service-ports dev bash + +# Inside container, run E2E tests +task e2e -- test/cli/path/to/test +``` + +**Run specific unit test** (can run locally or in Docker): ```bash # Run specific test by name go test -race -v ./path/to/pkg... -run TestName go test -race -v ./path/to/pkg... -run TestName/SubTest - -# Run specific E2E test -task e2e -- test/cli/path/to/test ``` **Verbose testing** (shows HTTP requests, ENVs, etcd operations): diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index 16a94584e9..aeed1065c4 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -41,6 +41,12 @@ The project uses ETCD for distributed storage and synchronization, with a custom For Docker-based development, see [Development Guide](docs/development.md). +**Important**: E2E tests MUST be run inside the Docker Compose dev container: +```bash +docker compose run --rm -u "$UID:$GID" --service-ports dev bash +# Then run: task e2e -- test/cli/path/to/test +``` + ### Local Machine Setup For development directly on your local machine without Docker, see [Local Development Guide](docs/local_development.md). @@ -80,9 +86,9 @@ docker-compose up -d ## CI/CD Pipeline ### Testing -- Unit tests: `task tests` -- Integration tests: `task tests` -- E2E tests: `task tests-cli` +- Unit tests: `task tests` (can run locally or in Docker) +- Integration tests: `task tests` (can run locally or in Docker) +- E2E tests: `task tests-cli` or `task e2e -- test/cli/path/to/test` (MUST run in Docker Compose dev container) ### Build Process 1. Code validation diff --git a/docs/CLI_OBJECT_LIFECYCLE.md b/docs/CLI_OBJECT_LIFECYCLE.md new file mode 100644 index 0000000000..38bee376a9 --- /dev/null +++ b/docs/CLI_OBJECT_LIFECYCLE.md @@ -0,0 +1,473 @@ +# CLI Object Lifecycle and State Management + +**CRITICAL READING for anyone working on CLI internals, mappers, or object state management.** + +This document explains how objects (branches, configs, rows, notifications, etc.) are created, loaded, saved, and tracked in the Keboola CLI. + +## Table of Contents + +1. [Core Concepts](#core-concepts) +2. [Object Lifecycle](#object-lifecycle) +3. [State vs Manifest](#state-vs-manifest) +4. [Registry.Set() - CRITICAL](#registryset---critical) +5. [Mappers and Their Timing](#mappers-and-their-timing) +6. [Pull Operation Flow](#pull-operation-flow) +7. [Push Operation Flow](#push-operation-flow) +8. [Common Pitfalls](#common-pitfalls) + +--- + +## Core Concepts + +### Object Components + +Each CLI object has THREE components: + +1. **Object** (`*model.Notification`, `*model.Config`, etc.) + - The actual data model with fields (event, recipient, filters, etc.) + - Contains business logic and validation rules + +2. **Manifest** (`*model.NotificationManifest`, `*model.ConfigManifest`, etc.) + - Metadata about WHERE the object is stored (paths, IDs) + - Tracked in `.keboola/manifest.json` for persistence across runs + - Parent-child relationships (Config → Notifications, Config → Rows) + +3. **State** (`*model.NotificationState`, `*model.ConfigState`, etc.) + - Combines Manifest + Object (Local and/or Remote versions) + - Tracks whether object exists locally, remotely, or both + - Managed by the Registry + +### State Fields + +```go +type NotificationState struct { + *NotificationManifest // WHERE it is + Local *Notification // Local filesystem version + Remote *Notification // Remote API version +} +``` + +**IMPORTANT:** During operations: +- **Pull**: Remote is set first, Local is set during save +- **Push**: Local is set first, Remote is set after API call +- **Both exist**: Both are set, diff engine compares them + +--- + +## Object Lifecycle + +### Creation Flow (Pull Operation) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Remote Load (remote/notification.go) │ +│ - Fetch from API │ +│ - u.loadObject(notification) creates Manifest + State │ +│ - loadObject sets Remote = Local = notification (copy) │ +│ - Set parent path on manifest │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. MapBeforeLocalSave (mapper/notification/local_save.go) │ +│ - Called ONCE PER NOTIFICATION (not per config) │ +│ - Writes config.json: {event, filters, recipient} │ +│ - Writes meta.json: {} │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. File Writing + Manifest Save (manifest/file.go) │ +│ - Write notification files to notifications/sub-{id}/ │ +│ - setRecords() populates ConfigManifest.Notifications │ +│ - Result: {"notifications": [{"id": "...", "path": ...}]} │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 4. Validation (state/state.go:validateLocal) │ +│ - Check Local state for all objects │ +│ - Validate required fields (event, recipient) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Loading Flow (After Files Exist) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Manifest Load (project/manifest/file.go) │ +│ - Read .keboola/manifest.json │ +│ - Create Manifests for all tracked objects │ +│ - Registry.GetOrCreateFrom() for each │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. Local Load (state/local/manager.go) │ +│ - Scan filesystem for object directories │ +│ - Match paths to manifests │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. MapAfterLocalLoad (mapper/notification/local_load.go) │ +│ - Called ONCE PER NOTIFICATION │ +│ - Reads config.json → notification.Event, Recipient │ +│ - No auto-filters in local file (added during push only) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## State vs Manifest + +### State (Registry) + +**What it is:** In-memory collection of ALL objects currently loaded + +**Where:** `internal/pkg/state/registry/registry.go` + +**Purpose:** +- Track all objects in this session +- Provide lookup by key +- Manage object relationships + +**Access:** +- `state.All()` - Get all objects +- `state.Get(key)` - Get specific object +- `state.Set(objectState)` - Add NEW object (fails if exists!) + +### Manifest + +**What it is:** Persistent record of which objects exist in the project + +**Where:** `.keboola/manifest.json` (file on disk) + +**Purpose:** +- Remember object IDs across runs +- Track parent-child relationships (config → rows, config → notifications) +- Enable diff operations (detect deletions) + +**Structure:** +```json +{ + "configurations": [ + { + "id": "config-123", + "path": "extractor/ex-generic-v2/my-config", + "rows": [{"id": "row-456"}], + "notifications": [{"id": "sub-789", "path": "notifications/sub-789"}] + } + ] +} +``` + +**When populated:** +- `ConfigManifest.Rows` - During manifest save via `setRecords()` +- `ConfigManifest.Notifications` - During manifest save via `setRecords()` (NOT in mapper) +- Saved by `manifest.Save()` which calls `setRecords(m.All())` + +--- + +## Registry.Set() - CRITICAL + +### **RULE: Set() is ONLY for NEW objects** + +```go +// ✅ CORRECT - Adding new object via loadObject (preferred pattern) +// loadObject handles manifest creation, Registry.GetOrCreateFrom(), setting Remote+Local +if err := u.loadObject(notification); err != nil { + return err +} + +// ❌ WRONG - Trying to update existing object +existingState, _ := registry.Get(key) +existingState.Local = newValue +registry.Set(existingState) // FAILS: "already exists" + +// ✅ CORRECT - Updating existing object +existingState, _ := registry.Get(key) +existingState.Local = newValue // Just assign, no Set() call needed +``` + +### Why Set() Fails on Existing Objects + +From `registry.go:304-306`: +```go +if _, found := s.objects.Get(key.String()); found { + return errors.Errorf(`object "%s" already exists`, key.Desc()) +} +``` + +**To modify existing objects: Just update fields directly** + +The Registry holds references, so changes to the object are immediately visible. + +--- + +## Mappers and Their Timing + +### Mapper Types + +#### LocalSaveMapper (MapBeforeLocalSave) +**When:** Before object is written to disk +**Use for:** +- Transform object → file content +- Create additional files (meta.json, description.md) +- Called ONCE PER OBJECT, not once per parent + +#### LocalLoadMapper (MapAfterLocalLoad) +**When:** After files are read from disk +**Use for:** +- Parse file content → object +- Normalize/validate data + +#### RemoteLoadMapper (MapAfterRemoteLoad) +**When:** After object is fetched from API +**Use for:** Rarely used, most remote work happens in remote/manager.go + +#### BeforePersistMapper (MapBeforePersist) +**When:** Before new object is added to manifest +**Use for:** Modify manifest record before first save + +### Execution Order (Pull) + +``` +Remote Load (u.loadObject per notification) + ↓ +MapBeforeLocalSave (called per notification) + ↓ +Write Files (config.json + meta.json per notification) + ↓ +Write Manifest (manifest.json) ← setRecords() populates ConfigManifest.Notifications HERE + ↓ +Validation +``` + +### Execution Order (Load from disk) + +``` +Manifest Load (reads notification IDs and paths) + ↓ +Local Load (scan filesystem, create states from manifest) + ↓ +MapAfterLocalLoad (reads config.json per notification) + ↓ +Validation +``` + +--- + +## Pull Operation Flow + +### Step-by-Step: Pulling Notifications + +```go +// 1. REMOTE LOAD (remote/notification.go:loadNotifications) +notification := model.NewNotification(apiSubscription) +notification.BranchID = parentConfig.BranchID // From parent config manifest +notification.ComponentID = parentConfig.ComponentID +notification.ConfigID = parentConfig.ID + +// loadObject creates manifest + state (Remote=Local=notification) +if err := u.loadObject(notification); err != nil { + return err +} + +// Set parent path after loadObject (needed for correct directory placement) +notificationState, found := u.state.Get(notification.Key()) +if !found { + continue +} +notificationState.Manifest().SetParentPath(parentConfig.Path()) + + +// 2. MAP BEFORE LOCAL SAVE (mapper/notification/local_save.go) +// Called ONCE for this specific notification (NOT iterating all states) +func (m *mapper) MapBeforeLocalSave(_ context.Context, recipe *model.LocalSaveRecipe) error { + notification, ok := recipe.Object.(*model.Notification) + if !ok { + return nil // Skip non-notifications + } + // Write config.json: {event, filters, recipient, expiresAt} + m.saveConfigFile(recipe, notification) + // Write meta.json: {} + m.saveMetaFile(recipe) + return nil +} + + +// 3. MANIFEST SAVE (project/manifest/file.go:setRecords) +// setRecords() iterates all states and populates ConfigManifest.Notifications +// Result: manifest.json has {"notifications": [{"id": "sub-123", "path": "notifications/sub-123"}]} +``` + +### Why This Flow? + +1. **Remote load creates states** - `loadObject` handles all manifest/state creation +2. **Mapper writes per-notification files** - Each notification gets its own `config.json` + `meta.json` +3. **Manifest save populates tracking** - `setRecords()` collects all notifications per config + +**CRITICAL:** `ConfigManifest.Notifications` is populated by `setRecords()` during manifest save, NOT by the mapper or remote load. + +--- + +## Push Operation Flow + +### Step-by-Step: Pushing Notifications + +```go +// 1. LOCAL LOAD (already happened) +// Manifest has {id, path} for each notification +// local_load.go reads config.json → notification.Event, Recipient (no auto-filters in local) +// State has Local set, Remote = nil (for new) or Remote set (for existing, from prior pull) + + +// 2. DIFF ENGINE (internal/pkg/diff) +// Compares Local vs Remote for each object +// New (no Remote): CREATE +// Changed (has Remote, fields differ): shows "changed: filters, recipient" etc. +// Note: on repeated push without pull, diff shows "changed: filters" because +// local has no auto-filters (branch.id, job.component.id, job.configuration.id) +// while remote has them. This is expected behavior. + + +// 3. REMOTE SAVE (remote/manager.go via remote/notification.go) +// New notification: createNotificationRequest() auto-adds branch/component/config filters +req := u.createNotificationRequest(notification) +// Updated notification: delete-old (WithOnSuccess callback) then create-new +// This is because the notifications API has no update endpoint. + + +// 4. SUCCESS CALLBACK +// Assigns API-assigned ID and sets remote state +notification.ID = created.ID +notification.CreatedAt = created.CreatedAt +notificationState.SetRemoteState(remoteNotification) +u.changes.AddCreated(notificationState) + +// ❌ DO NOT: Registry.Set(notificationState) +// State already exists from local load! + + +// 5. NO LOCAL FILE UPDATE +// Push never writes local files +// IDs in manifest.json are updated for new notifications via manifest.Save() +``` + +--- + +## Common Pitfalls + +### ❌ Pitfall 1: Calling Set() on Existing Objects + +```go +// WRONG +existingState, _ := m.state.Get(key) +existingState.Local = newValue +m.state.Set(existingState) // ERROR: already exists + +// CORRECT +existingState, _ := m.state.Get(key) +existingState.Local = newValue // Just assign, that's it! +``` + +### ❌ Pitfall 2: Manual State Construction Instead of loadObject + +```go +// WRONG - Manual manifest/state construction (old pattern) +manifest := &model.NotificationManifest{...} +manifest.SetParentPath(...) +state := &model.NotificationState{ + NotificationManifest: manifest, + Remote: notification, + Local: notification, +} +u.state.Set(state) + +// CORRECT - Use loadObject (handles everything) +if err := u.loadObject(notification); err != nil { + return err +} +notificationState, _ := u.state.Get(notification.Key()) +notificationState.Manifest().SetParentPath(parentConfig.Path()) +``` + +### ❌ Pitfall 3: Populating ConfigManifest.Notifications in Mapper + +```go +// WRONG - Mapper should not populate ConfigManifest.Notifications +func (m *mapper) MapBeforeLocalSave(ctx context.Context, recipe *model.LocalSaveRecipe) error { + config, ok := recipe.Object.(*model.Config) + if !ok { return nil } + // This is WRONG for notifications - setRecords() handles it during manifest.Save() + for _, notifState := range allNotifications { + config.Manifest.Notifications = append(config.Manifest.Notifications, ...) + } +} + +// CORRECT - setRecords() in manifest/file.go populates this automatically +// Just write the per-notification files in MapBeforeLocalSave: +func (m *mapper) MapBeforeLocalSave(ctx context.Context, recipe *model.LocalSaveRecipe) error { + notification, ok := recipe.Object.(*model.Notification) + if !ok { return nil } + m.saveConfigFile(recipe, notification) + m.saveMetaFile(recipe) + return nil +} +``` + +### ❌ Pitfall 4: Confusing State.All() with Manifest.All() + +```go +// State.All() - Returns ObjectState (has Local/Remote) +for _, objectState := range state.All() { + notif := objectState.(*NotificationState).Local +} + +// Manifest.All() - Returns ObjectManifest (has paths/IDs only) +for _, manifest := range manifest.All() { + notifManifest := manifest.(*NotificationManifest) + // No access to actual notification data here! +} +``` + +--- + +## Testing Checklist + +When implementing a new object type: + +- [ ] Remote load uses `u.loadObject()` (not manual manifest/state construction) +- [ ] `loadObject()` called before `SetParentPath()` on manifest +- [ ] `MapBeforeLocalSave` writes per-object files (not per-parent files) +- [ ] `MapAfterLocalLoad` reads per-object files, no registry manipulation +- [ ] `ConfigManifest.Notifications`/`Rows` populated by `setRecords()`, not by mapper +- [ ] Manifest custom JSON marshaling handles your object type +- [ ] `manifest/file.go:records()` includes your object type +- [ ] `manifest/file.go:setRecords()` handles your object type + +--- + +## References + +- State Registry: `internal/pkg/state/registry/registry.go` +- Manifest File: `internal/pkg/project/manifest/file.go` +- Mapper Interfaces: `internal/pkg/mapper/mapper.go` +- Example Notification Mappers: + - `internal/pkg/mapper/notification/local_save.go` + - `internal/pkg/mapper/notification/local_load.go` + - `internal/pkg/state/remote/notification.go` + +--- + +## Quick Reference Card + +``` +┌─────────────────────────────────────────────────────────────┐ +│ GOLDEN RULES │ +├─────────────────────────────────────────────────────────────┤ +│ 1. Registry.Set() ONLY for NEW objects │ +│ 2. Update existing: just assign fields, no Set() │ +│ 3. Pull: use loadObject() — creates manifest+state for you │ +│ 4. ConfigManifest.Notifications: populated by setRecords() │ +│ 5. MapBeforeLocalSave: called per-object, write object files │ +│ 6. MapAfterLocalLoad: called per-object, read object files │ +│ 7. Validation needs Local to be set (not nil) │ +└─────────────────────────────────────────────────────────────┘ +``` diff --git a/go.mod b/go.mod index 8a39ebefe8..7ea5226cb2 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/keboola/go-cloud-encrypt v0.0.0-20250422071622-41a5d5547c43 github.com/keboola/go-utils v1.4.0 - github.com/keboola/keboola-sdk-go/v2 v2.12.0 + github.com/keboola/keboola-sdk-go/v2 v2.13.1 github.com/klauspost/compress v1.18.4 github.com/klauspost/pgzip v1.2.6 github.com/kylelemons/godebug v1.1.0 @@ -105,6 +105,7 @@ require ( github.com/DataDog/datadog-agent/pkg/version v0.71.0 // indirect github.com/DataDog/go-libddwaf/v4 v4.8.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect github.com/bgentry/speakeasy v0.2.0 // indirect github.com/bitfield/gotestdox v0.2.2 // indirect github.com/cheggaaa/pb/v3 v3.1.6 // indirect @@ -188,17 +189,17 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/kms v1.23.0 // indirect - cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/kms v1.23.2 // indirect + cloud.google.com/go/longrunning v0.7.0 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect - cloud.google.com/go/storage v1.57.0 // indirect + cloud.google.com/go/storage v1.58.0 // indirect dario.cat/mergo v1.0.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect @@ -224,27 +225,26 @@ require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/apex/log v1.9.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/aws/aws-sdk-go v1.55.8 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect - github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.12 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bep/godartsass/v2 v2.5.0 // indirect github.com/bep/golibsass v1.2.0 // indirect @@ -297,8 +297,8 @@ require ( github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/wire v0.7.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect @@ -306,7 +306,6 @@ require ( github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/justinas/alice v1.2.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/reedsolomon v1.12.4 // indirect @@ -374,13 +373,13 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - gocloud.dev v0.43.0 // indirect + gocloud.dev v0.44.0 // indirect golang.org/x/image v0.35.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/perf v0.0.0-20260112171951-5abaabe9f1bd // indirect - google.golang.org/api v0.252.0 // indirect - google.golang.org/genproto v0.0.0-20251007200510-49b9836ed3ff // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/api v0.257.0 // indirect + google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/go.sum b/go.sum index 72f919e9eb..f90ec2ac03 100644 --- a/go.sum +++ b/go.sum @@ -14,18 +14,18 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= -cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= +cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/storage v1.57.0 h1:4g7NB7Ta7KetVbOMpCqy89C+Vg5VE8scqlSHUPm7Rds= -cloud.google.com/go/storage v1.57.0/go.mod h1:329cwlpzALLgJuu8beyJ/uvQznDHpa2U5lGjWednkzg= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= +cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4= @@ -34,8 +34,8 @@ github.com/ActiveState/vt10x v1.3.1 h1:7qi8BGXUEBghzBxfXSY0J77etO+L95PZQlwD7ay2m github.com/ActiveState/vt10x v1.3.1/go.mod h1:8wJKd36c9NmCfGyPyOJmkvyIMvbUPfHkfdS8zZlK19s= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= @@ -48,8 +48,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZb github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= @@ -158,48 +158,48 @@ github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+z github.com/autarch/testify v1.2.2 h1:9Q9V6zqhP7R6dv+zRUddv6kXKLo6ecQhnFRFWM71i1c= github.com/autarch/testify v1.2.2/go.mod h1:oDbHKfFv2/D5UtVrxkk90OKcb6P4/AqF1Pcf6ZbvDQo= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= -github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= -github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= -github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= -github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8= -github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= -github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.12 h1:ofHawDLJTI6ytDIji+g4dXQ6u2idzTb04tDlN9AS614= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.12/go.mod h1:f5pL4iLDfbcxj1SZcdRdIokBB5eHbuYPS/Fs9DwUPRQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 h1:w9LnHqTq8MEdlnyhV4Bwfizd65lfNCNgdlNC6mM5paE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9/go.mod h1:LGEP6EK4nj+bwWNdrvX/FnDTFowdBNwcSPuZu/ouFys= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0 h1:X0FveUndcZ3lKbSpIC6rMYGRiQTcUVRNH6X4yYtIrlU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0/go.mod h1:IWjQYlqw4EX9jw2g3qnEPPWvCE6bS8fKzhMed1OK7c8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 h1:wuZ5uW2uhJR63zwNlqWH2W4aL4ZjeJP3o92/W+odDY4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9/go.mod h1:/G58M2fGszCrOzvJUkDdY8O9kycodunH4VdT5oBAqls= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= +github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 h1:4GNV1lhyELGjMz5ILMRxDvxvOaeo3Ux9Z69S1EgVMMQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3/go.mod h1:br7KA6edAAqDGUYJ+zVVPAyMrPhnN+zdt17yTUT6FPw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo= github.com/aws/aws-sdk-go-v2/service/kms v1.41.2/go.mod h1:Pqd9k4TuespkireN206cK2QBsaBTL6X+VPAez5Qcijk= -github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4 h1:mUI3b885qJgfqKDUSj6RgbRqLdX0wGmg8ruM03zNfQA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4/go.mod h1:6v8ukAxc7z4x4oBjGUsLnH7KGLY9Uhcgij19UJNkiMg= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -505,11 +505,11 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= @@ -559,10 +559,6 @@ github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLany github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= @@ -589,8 +585,8 @@ github.com/keboola/go-oauth2-proxy/v7 v7.13.1-0.20251120082210-251fbcb18c16 h1:m github.com/keboola/go-oauth2-proxy/v7 v7.13.1-0.20251120082210-251fbcb18c16/go.mod h1:2KeAM0/QPbyUAoky+PXVgQDt/5m0qNcn30z9jh4ig8A= github.com/keboola/go-utils v1.4.0 h1:WTyj95yrr8O8HxtC8TSTyUcElZiRGDeEdVvDpFo6HUo= github.com/keboola/go-utils v1.4.0/go.mod h1:IopwJzFz2gh0Yj3fUbIe2eamRoDKzbXvjqFjQyw3ZdQ= -github.com/keboola/keboola-sdk-go/v2 v2.12.0 h1:jU9trH7EW3MzRI4Yz/wt5qlXHBo3guTe9mxGQ8OB/v4= -github.com/keboola/keboola-sdk-go/v2 v2.12.0/go.mod h1:N/PkJnEHcyHMbVjHPjTdQwj5b9Iajl7PEaFUVtywHKU= +github.com/keboola/keboola-sdk-go/v2 v2.13.1 h1:vJtszujui7ABejOIj3GxkasJ0BjHbzpr/vfhdYV5E2c= +github.com/keboola/keboola-sdk-go/v2 v2.13.1/go.mod h1:rNZ6G/UVpVICOLPhNn59jH+EBvkVo78BwRIIG0pMhA4= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -1027,8 +1023,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWS go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= go.opentelemetry.io/otel/log/logtest v0.13.0 h1:xxaIcgoEEtnwdgj6D6Uo9K/Dynz9jqIxSDu2YObJ69Q= @@ -1072,8 +1068,8 @@ goa.design/goa/v3 v3.24.3 h1:r63sFxuTH7WESS0/1GpXiJUVuYADUyLkYhlhe4JcEIo= goa.design/goa/v3 v3.24.3/go.mod h1:VZ8CcXJRZh09ijtNJJS2gNyKufpmrM+Ul/Qy3viwcOU= goa.design/plugins/v3 v3.24.3 h1:qx45NS/6mGbTf27hyXARCH06R0K2wWyimBptTJJN2to= goa.design/plugins/v3 v3.24.3/go.mod h1:NQr8dM8XR1gCOGlHA4WPbB96CRgS+WAEAUMDa9CPdz8= -gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M= -gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4= +gocloud.dev v0.44.0 h1:iVyMAqFl2r6xUy7M4mfqwlN+21UpJoEtgHEcfiLMUXs= +gocloud.dev v0.44.0/go.mod h1:ZmjROXGdC/eKZLF1N+RujDlFRx3D+4Av2thREKDMVxY= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1221,8 +1217,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6f gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI= -google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw= +google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= +google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -1233,10 +1229,10 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20251007200510-49b9836ed3ff h1:3jGSSqkLOAYU1gI52uHoj51zxEsGMEYatnBFU0m6pB8= -google.golang.org/genproto v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:45Y7O/+fGjlhL8+FRpuLqM9YKvn+AU5dolRkE3DOaX8= -google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE= -google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= diff --git a/internal/pkg/fixtures/fixtures.go b/internal/pkg/fixtures/fixtures.go index aea74f51be..f0553f46ff 100644 --- a/internal/pkg/fixtures/fixtures.go +++ b/internal/pkg/fixtures/fixtures.go @@ -19,11 +19,12 @@ import ( ) type ProjectSnapshot struct { - Branches []*BranchWithConfigs `json:"branches"` - Schedules []*Schedule `json:"schedules,omitempty"` - Sandboxes []*Sandbox `json:"sandboxes,omitempty"` - Buckets []*Bucket `json:"buckets,omitempty"` - Files []*File `json:"files,omitempty"` + Branches []*BranchWithConfigs `json:"branches"` + Schedules []*Schedule `json:"schedules,omitempty"` + Sandboxes []*Sandbox `json:"sandboxes,omitempty"` + Buckets []*Bucket `json:"buckets,omitempty"` + Files []*File `json:"files,omitempty"` + NotificationSubscriptions []*keboola.NotificationSubscription `json:"notificationSubscriptions,omitempty"` } type Branch struct { @@ -113,14 +114,15 @@ type BackendDefinition struct { } type StateFile struct { - Backend *BackendDefinition `json:"backend,omitempty"` - LegacyTransformation bool `json:"legacyTransformation,omitempty"` - AllBranchesConfigs []string `json:"allBranchesConfigs" validate:"required"` - Branches []*BranchState `json:"branches" validate:"required"` - Buckets []*Bucket `json:"buckets,omitempty"` - Sandboxes []*Sandbox `json:"sandboxes,omitempty"` - Files []*File `json:"files,omitempty"` - Envs map[string]string `json:"envs,omitempty"` // additional ENVs + Backend *BackendDefinition `json:"backend,omitempty"` + LegacyTransformation bool `json:"legacyTransformation,omitempty"` + AllBranchesConfigs []string `json:"allBranchesConfigs" validate:"required"` + Branches []*BranchState `json:"branches" validate:"required"` + Buckets []*Bucket `json:"buckets,omitempty"` + Sandboxes []*Sandbox `json:"sandboxes,omitempty"` + Files []*File `json:"files,omitempty"` + NotificationSubscriptions []*keboola.NotificationSubscription `json:"notificationSubscriptions,omitempty"` + Envs map[string]string `json:"envs,omitempty"` // additional ENVs } // ToAPI maps fixture to model.Branch. diff --git a/internal/pkg/mapper/ignore/remote.go b/internal/pkg/mapper/ignore/remote.go index 99efff38bf..df2daf0d39 100644 --- a/internal/pkg/mapper/ignore/remote.go +++ b/internal/pkg/mapper/ignore/remote.go @@ -52,6 +52,8 @@ func (m *ignoreMapper) isIgnored(ctx context.Context, object model.Object) bool return m.isIgnoredConfig(ctx, configState.RemoteState().(*model.Config)) } return false + case *model.Notification: + return false default: panic(errors.Errorf(`unexpected object type: %T`, object)) } diff --git a/internal/pkg/mapper/mapper.go b/internal/pkg/mapper/mapper.go index ca6a20e644..8d7538bcff 100644 --- a/internal/pkg/mapper/mapper.go +++ b/internal/pkg/mapper/mapper.go @@ -11,6 +11,11 @@ import ( // LocalSaveMapper is intended to modify how the object will be saved in the filesystem. // If you need a list of all saved objects, when they are already saved, use the AfterLocalOperationListener instead. +// +// Note: parent-child manifest relationships (e.g., ConfigManifest.Notifications) are populated +// automatically by setRecords() during manifest loading, not by this mapper. +// MapBeforeLocalSave is used to write object files (config.json, meta.json, etc.) to the filesystem. +// See docs/CLI_OBJECT_LIFECYCLE.md for the complete flow. type LocalSaveMapper interface { MapBeforeLocalSave(ctx context.Context, recipe *model.LocalSaveRecipe) error } @@ -19,6 +24,12 @@ type LocalSaveMapper interface { // If you need a list of all loaded objects use AfterLocalOperationListener instead. // Important: do not rely on other objects in the LocalLoadMapper, they may not be loaded yet. // If you need to work with multiple objects (and relationships between them), use the AfterLocalOperationListener instead. +// +// CRITICAL: When loading objects that may already exist in state (e.g., from manifest load): +// - Use registry.Get() to check if state exists +// - If exists: Update state.Local field directly - do NOT call registry.Set() +// - If new: Create state and call registry.Set() +// See docs/CLI_OBJECT_LIFECYCLE.md for examples. type LocalLoadMapper interface { MapAfterLocalLoad(ctx context.Context, recipe *model.LocalLoadRecipe) error } diff --git a/internal/pkg/mapper/notification/local_load.go b/internal/pkg/mapper/notification/local_load.go new file mode 100644 index 0000000000..b2d04c877d --- /dev/null +++ b/internal/pkg/mapper/notification/local_load.go @@ -0,0 +1,48 @@ +package notification + +import ( + "context" + + "github.com/keboola/keboola-as-code/internal/pkg/filesystem" + "github.com/keboola/keboola-as-code/internal/pkg/model" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +// MapAfterLocalLoad reads notification config.json and registers meta.json. +func (m *mapper) MapAfterLocalLoad(ctx context.Context, recipe *model.LocalLoadRecipe) error { + notification, ok := recipe.Object.(*model.Notification) + if !ok { + return nil + } + + errs := errors.NewMultiError() + if err := m.loadConfigFile(ctx, recipe, notification); err != nil { + errs.Append(err) + } + if err := m.loadMetaFile(ctx, recipe); err != nil { + errs.Append(err) + } + return errs.ErrorOrNil() +} + +func (m *mapper) loadConfigFile(ctx context.Context, recipe *model.LocalLoadRecipe, notification *model.Notification) error { + _, err := recipe.Files. + Load(m.state.NamingGenerator().ConfigFilePath(recipe.ObjectManifest.Path())). + AddMetadata(filesystem.ObjectKeyMetadata, recipe.Key()). + SetDescription(recipe.ObjectManifest.Kind().Name). + AddTag(model.FileTypeJSON). + AddTag(model.FileKindObjectConfig). + ReadJSONFileTo(ctx, notification) + return err +} + +func (m *mapper) loadMetaFile(ctx context.Context, recipe *model.LocalLoadRecipe) error { + _, err := recipe.Files. + Load(m.state.NamingGenerator().MetaFilePath(recipe.ObjectManifest.Path())). + AddMetadata(filesystem.ObjectKeyMetadata, recipe.Key()). + SetDescription(recipe.ObjectManifest.Kind().Name + " metadata"). + AddTag(model.FileTypeJSON). + AddTag(model.FileKindObjectMeta). + ReadJSONFile(ctx) + return err +} diff --git a/internal/pkg/mapper/notification/local_load_test.go b/internal/pkg/mapper/notification/local_load_test.go new file mode 100644 index 0000000000..0ed023ac30 --- /dev/null +++ b/internal/pkg/mapper/notification/local_load_test.go @@ -0,0 +1,70 @@ +package notification_test + +import ( + "testing" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/keboola/keboola-as-code/internal/pkg/filesystem" + "github.com/keboola/keboola-as-code/internal/pkg/mapper/notification" + "github.com/keboola/keboola-as-code/internal/pkg/model" + "github.com/keboola/keboola-as-code/internal/pkg/service/common/dependencies" +) + +func TestNotificationMapper_MapAfterLocalLoad(t *testing.T) { + t.Parallel() + d := dependencies.NewMocked(t, t.Context()) + logger := d.DebugLogger() + mockedState := d.MockedState() + mockedState.Mapper().AddMapper(notification.NewMapper(mockedState)) + + notifKey := model.NotificationKey{ + BranchID: 123, + ComponentID: keboola.ComponentID("ex-generic-v2"), + ConfigID: keboola.ConfigID("my-config"), + ID: keboola.NotificationSubscriptionID("sub-123"), + } + manifest := &model.NotificationManifest{ + NotificationKey: notifKey, + Paths: model.Paths{ + AbsPath: model.NewAbsPath("main/extractor/ex-generic-v2/my-config", "notifications/sub-123"), + }, + } + + // Write config.json and meta.json to the mocked FS. + fs := mockedState.ObjectsRoot() + ctx := t.Context() + require.NoError(t, fs.Mkdir(ctx, manifest.Path())) + configJSON := `{ + "event": "job-failed", + "filters": [{"field": "job.configuration.id", "value": "my-config", "operator": "=="}], + "recipient": {"channel": "email", "address": "user@example.com"} + }` + require.NoError(t, fs.WriteFile(ctx, filesystem.NewRawFile( + mockedState.NamingGenerator().ConfigFilePath(manifest.Path()), + configJSON, + ))) + require.NoError(t, fs.WriteFile(ctx, filesystem.NewRawFile( + mockedState.NamingGenerator().MetaFilePath(manifest.Path()), + `{}`, + ))) + + notif := &model.Notification{NotificationKey: notifKey} + recipe := model.NewLocalLoadRecipe(mockedState.FileLoader(), manifest, notif) + require.NoError(t, mockedState.Mapper().MapAfterLocalLoad(ctx, recipe)) + assert.Empty(t, logger.WarnAndErrorMessages()) + + assert.Equal(t, keboola.NotificationEventJobFailed, notif.Event) + assert.Equal(t, keboola.NotificationRecipient{ + Channel: keboola.NotificationChannelEmail, + Address: "user@example.com", + }, notif.Recipient) + require.Len(t, notif.Filters, 1) + assert.Equal(t, keboola.NotificationFilter{ + Field: "job.configuration.id", + Value: "my-config", + Operator: keboola.NotificationFilterOperatorEquals, + }, notif.Filters[0]) +} diff --git a/internal/pkg/mapper/notification/local_save.go b/internal/pkg/mapper/notification/local_save.go new file mode 100644 index 0000000000..6a0b0b22d0 --- /dev/null +++ b/internal/pkg/mapper/notification/local_save.go @@ -0,0 +1,63 @@ +package notification + +import ( + "context" + + "github.com/keboola/go-utils/pkg/orderedmap" + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + + "github.com/keboola/keboola-as-code/internal/pkg/encoding/json" + "github.com/keboola/keboola-as-code/internal/pkg/filesystem" + "github.com/keboola/keboola-as-code/internal/pkg/model" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +// MapBeforeLocalSave writes notification config.json and meta.json files. +func (m *mapper) MapBeforeLocalSave(_ context.Context, recipe *model.LocalSaveRecipe) error { + notification, ok := recipe.Object.(*model.Notification) + if !ok { + return nil + } + + if err := m.saveConfigFile(recipe, notification); err != nil { + return err + } + m.saveMetaFile(recipe) + return nil +} + +func (m *mapper) saveConfigFile(recipe *model.LocalSaveRecipe, notification *model.Notification) error { + type configContent struct { + Event keboola.NotificationEvent `json:"event"` + Filters []keboola.NotificationFilter `json:"filters,omitempty"` + Recipient keboola.NotificationRecipient `json:"recipient"` + ExpiresAt *keboola.NotificationExpiration `json:"expiresAt,omitempty"` + } + + content := configContent{ + Event: notification.Event, + Filters: notification.Filters, + Recipient: notification.Recipient, + ExpiresAt: notification.ExpiresAt, + } + + om := orderedmap.New() + if err := json.ConvertByJSON(content, om); err != nil { + return errors.Errorf("failed to convert notification to orderedmap: %w", err) + } + + path := m.state.NamingGenerator().ConfigFilePath(recipe.Path()) + recipe.Files. + Add(filesystem.NewJSONFile(path, om)). + AddTag(model.FileTypeJSON). + AddTag(model.FileKindObjectConfig) + return nil +} + +func (m *mapper) saveMetaFile(recipe *model.LocalSaveRecipe) { + path := m.state.NamingGenerator().MetaFilePath(recipe.Path()) + recipe.Files. + Add(filesystem.NewJSONFile(path, orderedmap.New())). + AddTag(model.FileTypeJSON). + AddTag(model.FileKindObjectMeta) +} diff --git a/internal/pkg/mapper/notification/local_save_test.go b/internal/pkg/mapper/notification/local_save_test.go new file mode 100644 index 0000000000..c5686e7eb9 --- /dev/null +++ b/internal/pkg/mapper/notification/local_save_test.go @@ -0,0 +1,80 @@ +package notification_test + +import ( + "testing" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/keboola/keboola-as-code/internal/pkg/mapper/notification" + "github.com/keboola/keboola-as-code/internal/pkg/model" + "github.com/keboola/keboola-as-code/internal/pkg/service/common/dependencies" +) + +func TestNotificationMapper_MapBeforeLocalSave(t *testing.T) { + t.Parallel() + d := dependencies.NewMocked(t, t.Context()) + logger := d.DebugLogger() + mockedState := d.MockedState() + mockedState.Mapper().AddMapper(notification.NewMapper(mockedState)) + + notifKey := model.NotificationKey{ + BranchID: 123, + ComponentID: keboola.ComponentID("ex-generic-v2"), + ConfigID: keboola.ConfigID("my-config"), + ID: keboola.NotificationSubscriptionID("sub-123"), + } + manifest := &model.NotificationManifest{ + NotificationKey: notifKey, + Paths: model.Paths{ + AbsPath: model.NewAbsPath("main/extractor/ex-generic-v2/my-config", "notifications/sub-123"), + }, + } + notif := &model.Notification{ + NotificationKey: notifKey, + Event: keboola.NotificationEventJobFailed, + Recipient: keboola.NotificationRecipient{ + Channel: keboola.NotificationChannelEmail, + Address: "user@example.com", + }, + Filters: []keboola.NotificationFilter{ + { + Field: "job.configuration.id", + Value: "my-config", + Operator: keboola.NotificationFilterOperatorEquals, + }, + }, + } + state := &model.NotificationState{ + NotificationManifest: manifest, + Local: notif, + } + + recipe := model.NewLocalSaveRecipe(state.Manifest(), state.Local, model.NewChangedFields()) + require.NoError(t, mockedState.Mapper().MapBeforeLocalSave(t.Context(), recipe)) + assert.Empty(t, logger.WarnAndErrorMessages()) + + assert.Len(t, recipe.Files.All(), 2) + + configPath := mockedState.NamingGenerator().ConfigFilePath(manifest.Path()) + metaPath := mockedState.NamingGenerator().MetaFilePath(manifest.Path()) + + configFile := recipe.Files.GetOneByTag(model.FileKindObjectConfig) + require.NotNil(t, configFile) + assert.Equal(t, configPath, configFile.Path()) + configRaw, err := configFile.ToRawFile() + require.NoError(t, err) + assert.JSONEq(t, `{ + "event": "job-failed", + "filters": [{"field": "job.configuration.id", "value": "my-config", "operator": "=="}], + "recipient": {"channel": "email", "address": "user@example.com"} + }`, configRaw.Content) + + metaFile := recipe.Files.GetOneByTag(model.FileKindObjectMeta) + require.NotNil(t, metaFile) + assert.Equal(t, metaPath, metaFile.Path()) + metaRaw, err := metaFile.ToRawFile() + require.NoError(t, err) + assert.JSONEq(t, `{}`, metaRaw.Content) +} diff --git a/internal/pkg/mapper/notification/notification.go b/internal/pkg/mapper/notification/notification.go new file mode 100644 index 0000000000..034c87d8c2 --- /dev/null +++ b/internal/pkg/mapper/notification/notification.go @@ -0,0 +1,13 @@ +package notification + +import ( + "github.com/keboola/keboola-as-code/internal/pkg/state" +) + +type mapper struct { + state *state.State +} + +func NewMapper(s *state.State) *mapper { + return &mapper{state: s} +} diff --git a/internal/pkg/model/keys.go b/internal/pkg/model/keys.go index 6f9ef8940e..444389b017 100644 --- a/internal/pkg/model/keys.go +++ b/internal/pkg/model/keys.go @@ -8,21 +8,23 @@ import ( ) const ( - BranchKind = "branch" - ComponentKind = "component" - ConfigKind = "config" - ConfigRowKind = "config row" - BlockKind = "block" - CodeKind = "code" - PhaseKind = "phase" - TaskKind = "task" - BranchAbbr = "B" - ConfigAbbr = "C" - RowAbbr = "R" - BlockAbbr = "b" - CodeAbbr = "c" - PhaseAbbr = "p" - TaskAbbr = "t" + BranchKind = "branch" + ComponentKind = "component" + ConfigKind = "config" + ConfigRowKind = "config row" + BlockKind = "block" + CodeKind = "code" + PhaseKind = "phase" + TaskKind = "task" + NotificationKind = "notification" + BranchAbbr = "B" + ConfigAbbr = "C" + RowAbbr = "R" + BlockAbbr = "b" + CodeAbbr = "c" + PhaseAbbr = "p" + TaskAbbr = "t" + NotificationAbbr = "N" ) type Key interface { @@ -82,6 +84,13 @@ type TaskKey struct { Index int `json:"-" validate:"min=0"` } +type NotificationKey struct { + BranchID keboola.BranchID `json:"branchId,omitempty"` + ComponentID keboola.ComponentID `json:"componentId,omitempty"` + ConfigID keboola.ConfigID `json:"configId,omitempty"` + ID keboola.NotificationSubscriptionID `json:"id,omitempty" validate:"omitempty"` +} + func (k BranchKey) Kind() Kind { return Kind{Name: BranchKind, Abbr: BranchAbbr} } @@ -110,6 +119,10 @@ func (k TaskKey) Kind() Kind { return Kind{Name: TaskKind, Abbr: TaskAbbr} } +func (k NotificationKey) Kind() Kind { + return Kind{Name: NotificationKind, Abbr: NotificationAbbr} +} + func (k BranchKey) ObjectID() string { return k.ID.String() } @@ -138,6 +151,10 @@ func (k TaskKey) ObjectID() string { return cast.ToString(k.Index) } +func (k NotificationKey) ObjectID() string { + return string(k.ID) +} + func (k BranchKey) Level() int { return 1 } @@ -166,6 +183,10 @@ func (k TaskKey) Level() int { return 6 } +func (k NotificationKey) Level() int { + return 4 +} + func (k BranchKey) Key() Key { return k } @@ -190,6 +211,10 @@ func (k TaskKey) Key() Key { return k } +func (k NotificationKey) Key() Key { + return k +} + func (k BlockKey) ConfigKey() Key { return ConfigKey{ BranchID: k.BranchID, @@ -263,6 +288,10 @@ func (k TaskKey) Desc() string { return fmt.Sprintf(`%s "branch:%d/component:%s/config:%s/phase:%d/task:%d"`, k.Kind().Name, k.BranchID, k.ComponentID, k.ConfigID, k.PhaseKey.Index, k.Index) } +func (k NotificationKey) Desc() string { + return fmt.Sprintf(`%s "branch:%d/component:%s/config:%s/notification:%s"`, k.Kind().Name, k.BranchID, k.ComponentID, k.ConfigID, k.ID) +} + func (k BranchKey) String() string { return fmt.Sprintf("%02d_%d_branch", k.Level(), k.ID) } @@ -295,6 +324,10 @@ func (k TaskKey) String() string { return fmt.Sprintf("%02d_%d_%s_%s_%03d_%03d_task", k.Level(), k.BranchID, k.ComponentID, k.ConfigID, k.PhaseKey.Index, k.Index) } +func (k NotificationKey) String() string { + return fmt.Sprintf("%02d_%d_%s_%s_%s_notification", k.Level(), k.BranchID, k.ComponentID, k.ConfigID, k.ID) +} + func (k ConfigKey) BranchKey() BranchKey { return BranchKey{ID: k.BranchID} } @@ -347,6 +380,18 @@ func (k TaskKey) ParentKey() (Key, error) { return k.PhaseKey, nil } +func (k NotificationKey) ConfigKey() ConfigKey { + return ConfigKey{ + BranchID: k.BranchID, + ComponentID: k.ComponentID, + ID: k.ConfigID, + } +} + +func (k NotificationKey) ParentKey() (Key, error) { + return k.ConfigKey(), nil +} + type ConfigIDMetadata struct { IDInTemplate keboola.ConfigID `json:"idInTemplate"` } diff --git a/internal/pkg/model/keys_test.go b/internal/pkg/model/keys_test.go new file mode 100644 index 0000000000..c1c42cdf3c --- /dev/null +++ b/internal/pkg/model/keys_test.go @@ -0,0 +1,142 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNotificationKey_Kind(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + kind := key.Kind() + assert.Equal(t, NotificationKind, kind.Name) + assert.Equal(t, NotificationAbbr, kind.Abbr) +} + +func TestNotificationKey_ObjectID(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + assert.Equal(t, "notif-123", key.ObjectID()) +} + +func TestNotificationKey_Level(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + assert.Equal(t, 4, key.Level()) +} + +func TestNotificationKey_Key(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + assert.Equal(t, key, key.Key()) +} + +func TestNotificationKey_Desc(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + expected := `notification "branch:123/component:keboola.orchestrator/config:456/notification:notif-123"` + assert.Equal(t, expected, key.Desc()) +} + +func TestNotificationKey_String(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + expected := "04_123_keboola.orchestrator_456_notif-123_notification" + assert.Equal(t, expected, key.String()) +} + +func TestNotificationKey_ConfigKey(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + expected := ConfigKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ID: "456", + } + assert.Equal(t, expected, key.ConfigKey()) +} + +func TestNotificationKey_ParentKey(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + expected := ConfigKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ID: "456", + } + parent, err := key.ParentKey() + require.NoError(t, err) + assert.Equal(t, expected, parent) +} + +func TestNotificationKey_ImplementsKeyInterface(t *testing.T) { + t.Parallel() + + key := NotificationKey{ + BranchID: 123, + ComponentID: "keboola.orchestrator", + ConfigID: "456", + ID: "notif-123", + } + + // Verify that NotificationKey implements the Key interface + var _ Key = key +} diff --git a/internal/pkg/model/manifest.go b/internal/pkg/model/manifest.go index a7a949125f..c54278277d 100644 --- a/internal/pkg/model/manifest.go +++ b/internal/pkg/model/manifest.go @@ -90,7 +90,8 @@ type ConfigRowManifest struct { type ConfigManifestWithRows struct { ConfigManifest - Rows []*ConfigRowManifest `json:"rows"` + Rows []*ConfigRowManifest `json:"rows"` + Notifications []*NotificationManifest `json:"notifications,omitempty"` } func (p *Paths) ClearRelatedPaths() { diff --git a/internal/pkg/model/notification.go b/internal/pkg/model/notification.go new file mode 100644 index 0000000000..e14a0be777 --- /dev/null +++ b/internal/pkg/model/notification.go @@ -0,0 +1,57 @@ +package model + +import ( + "time" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" +) + +// Notification represents a notification subscription in the Keboola project. +type Notification struct { + NotificationKey + Event keboola.NotificationEvent `json:"event" validate:"required" diff:"true"` + Filters []keboola.NotificationFilter `json:"filters,omitempty" diff:"true"` + Recipient keboola.NotificationRecipient `json:"recipient" validate:"required" diff:"true"` + ExpiresAt *keboola.NotificationExpiration `json:"expiresAt,omitempty" diff:"true"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + Relations Relations `json:"-" validate:"dive" diff:"true"` +} + +// NewNotification creates a notification model from API values. +// Note: BranchID, ComponentID, and ConfigID must be set separately after creation +// as they are not returned by the notification API. +func NewNotification(apiValue *keboola.NotificationSubscription) *Notification { + out := &Notification{} + out.ID = apiValue.ID + out.Event = apiValue.Event + out.Filters = apiValue.Filters + out.Recipient = apiValue.Recipient + out.ExpiresAt = apiValue.ExpiresAt + out.CreatedAt = apiValue.CreatedAt + return out +} + +// ObjectName returns the string representation of the notification for display. +func (n *Notification) ObjectName() string { + return string(n.ID) +} + +// SetObjectID sets the notification subscription ID. +func (n *Notification) SetObjectID(objectID any) { + n.ID = objectID.(keboola.NotificationSubscriptionID) +} + +// GetRelations returns relations of the notification. +func (n *Notification) GetRelations() Relations { + return n.Relations +} + +// SetRelations sets relations for the notification. +func (n *Notification) SetRelations(relations Relations) { + n.Relations = relations +} + +// AddRelation adds a relation to the notification. +func (n *Notification) AddRelation(relation Relation) { + n.Relations = append(n.Relations, relation) +} diff --git a/internal/pkg/model/notification_validation.go b/internal/pkg/model/notification_validation.go new file mode 100644 index 0000000000..c4a02e6e28 --- /dev/null +++ b/internal/pkg/model/notification_validation.go @@ -0,0 +1,116 @@ +package model + +import ( + "slices" + "sort" + "strings" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +func validNotificationFilterFields() []string { + return []string{ + "branch.id", + "job.id", + "job.component.id", + "job.configuration.id", + "job.token.id", + "project.id", + "eventType", + } +} + +func deprecatedNotificationFilterFieldNames() map[string]string { + return map[string]string{ + "configId": "job.configuration.id", + "componentId": "job.component.id", + "branchId": "branch.id", + "jobId": "job.id", + "tokenId": "job.token.id", + "projectId": "project.id", + "configuration": "job.configuration.id", + "component": "job.component.id", + "branch": "branch.id", + "job": "job.id", + "token": "job.token.id", + "project": "project.id", + } +} + +// ValidateNotificationFilters validates that all filter field names are valid. +// It returns an error if any deprecated or invalid field names are found. +func ValidateNotificationFilters(filters []keboola.NotificationFilter) error { + for i, filter := range filters { + if err := validateFilterField(filter.Field, i); err != nil { + return err + } + } + return nil +} + +func validateFilterField(fieldName string, index int) error { + validFields := validNotificationFilterFields() + deprecatedFields := deprecatedNotificationFilterFieldNames() + + // Check if field is valid + if slices.Contains(validFields, fieldName) { + return nil + } + + // Check if it's a deprecated field name + if correctName, ok := deprecatedFields[fieldName]; ok { + return errors.Errorf( + `filter[%d] uses deprecated field name "%s". Use "%s" instead`, + index, + fieldName, + correctName, + ) + } + + // Field name is invalid - suggest alternatives + suggestions := findSimilarFieldNames(fieldName) + if len(suggestions) > 0 { + return errors.Errorf( + `filter[%d] has invalid field name "%s". Did you mean one of: %s?`, + index, + fieldName, + strings.Join(suggestions, ", "), + ) + } + + return errors.Errorf( + `filter[%d] has invalid field name "%s". Valid field names are: %s`, + index, + fieldName, + strings.Join(validFields, ", "), + ) +} + +func findSimilarFieldNames(input string) []string { + validFields := validNotificationFilterFields() + deprecatedFields := deprecatedNotificationFilterFieldNames() + + var suggestions []string + lowerInput := strings.ToLower(input) + + // Check if input contains any part of valid field names + for _, validField := range validFields { + lowerValid := strings.ToLower(validField) + // If input contains part of valid field, or valid field contains part of input + if strings.Contains(lowerValid, lowerInput) || strings.Contains(lowerInput, lowerValid) { + suggestions = append(suggestions, validField) + } + } + + // Also check deprecated names that contain the input + for deprecatedName, correctName := range deprecatedFields { + if strings.Contains(strings.ToLower(deprecatedName), lowerInput) && !slices.Contains(suggestions, correctName) { + suggestions = append(suggestions, correctName) + } + } + + sort.Strings(suggestions) + return suggestions +} diff --git a/internal/pkg/model/notification_validation_test.go b/internal/pkg/model/notification_validation_test.go new file mode 100644 index 0000000000..10bbfad6f6 --- /dev/null +++ b/internal/pkg/model/notification_validation_test.go @@ -0,0 +1,152 @@ +package model + +import ( + "testing" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateNotificationFilters_ValidFields(t *testing.T) { + t.Parallel() + + validFilters := []keboola.NotificationFilter{ + {Field: "branch.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "123"}, + {Field: "job.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "456"}, + {Field: "job.component.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "ex-generic-v2"}, + {Field: "job.configuration.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "789"}, + {Field: "job.token.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "token-123"}, + {Field: "project.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "proj-456"}, + {Field: "eventType", Operator: keboola.NotificationFilterOperatorEquals, Value: "job-failed"}, + } + + err := ValidateNotificationFilters(validFilters) + assert.NoError(t, err, "valid filter fields should not return error") +} + +func TestValidateNotificationFilters_DeprecatedFields(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + field string + correctField string + }{ + {"configId", "configId", "job.configuration.id"}, + {"componentId", "componentId", "job.component.id"}, + {"branchId", "branchId", "branch.id"}, + {"jobId", "jobId", "job.id"}, + {"tokenId", "tokenId", "job.token.id"}, + {"projectId", "projectId", "project.id"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + filters := []keboola.NotificationFilter{ + {Field: tt.field, Operator: keboola.NotificationFilterOperatorEquals, Value: "123"}, + } + + err := ValidateNotificationFilters(filters) + require.Error(t, err, "deprecated field should return error") + assert.Contains(t, err.Error(), "deprecated field name") + assert.Contains(t, err.Error(), tt.field) + assert.Contains(t, err.Error(), tt.correctField) + }) + } +} + +func TestValidateNotificationFilters_InvalidFields(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + field string + }{ + {"completely_invalid", "foobar"}, + {"another_invalid", "xyz123"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + filters := []keboola.NotificationFilter{ + {Field: tt.field, Operator: keboola.NotificationFilterOperatorEquals, Value: "123"}, + } + + err := ValidateNotificationFilters(filters) + require.Error(t, err, "invalid field should return error") + assert.Contains(t, err.Error(), "invalid field name") + assert.Contains(t, err.Error(), tt.field) + }) + } +} + +func TestValidateNotificationFilters_EmptyFilters(t *testing.T) { + t.Parallel() + + err := ValidateNotificationFilters([]keboola.NotificationFilter{}) + assert.NoError(t, err, "empty filters should not return error") +} + +func TestValidateNotificationFilters_MultipleFilters(t *testing.T) { + t.Parallel() + + filters := []keboola.NotificationFilter{ + {Field: "branch.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "123"}, + {Field: "configId", Operator: keboola.NotificationFilterOperatorEquals, Value: "456"}, // deprecated + {Field: "job.token.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "789"}, + } + + err := ValidateNotificationFilters(filters) + require.Error(t, err, "should fail on first invalid filter") + assert.Contains(t, err.Error(), "filter[1]") + assert.Contains(t, err.Error(), "configId") +} + +func TestFindSimilarFieldNames(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expectedSuggestions []string + }{ + { + name: "config", + input: "config", + expectedSuggestions: []string{"job.configuration.id"}, + }, + { + name: "job", + input: "job", + expectedSuggestions: []string{"job.id", "job.component.id", "job.configuration.id", "job.token.id"}, + }, + { + name: "branch", + input: "branch", + expectedSuggestions: []string{"branch.id"}, + }, + { + name: "completely_invalid", + input: "xyz", + expectedSuggestions: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + suggestions := findSimilarFieldNames(tt.input) + + // Check that all expected suggestions are present + for _, expected := range tt.expectedSuggestions { + assert.Contains(t, suggestions, expected, "should contain suggested field") + } + }) + } +} diff --git a/internal/pkg/model/notificationmanifest.go b/internal/pkg/model/notificationmanifest.go new file mode 100644 index 0000000000..d76e5324df --- /dev/null +++ b/internal/pkg/model/notificationmanifest.go @@ -0,0 +1,61 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" +) + +// NotificationManifest represents a notification manifest record. +type NotificationManifest struct { + RecordState `json:"-"` + NotificationKey + Paths + Relations Relations `json:"relations,omitempty" validate:"dive"` +} + +// MarshalJSON serializes only the ID and path, omitting the embedded NotificationKey fields +// (branchId, componentId, configId) which would otherwise appear in the manifest JSON. +func (n *NotificationManifest) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID keboola.NotificationSubscriptionID `json:"id"` + Path string `json:"path,omitempty"` + }{ + ID: n.ID, + Path: n.GetRelativePath(), + }) +} + +// NewEmptyObject creates an empty Notification object with the key from this manifest. +func (n NotificationManifest) NewEmptyObject() Object { + return &Notification{NotificationKey: n.NotificationKey} +} + +// NewObjectState creates a new NotificationState wrapping this manifest. +func (n *NotificationManifest) NewObjectState() ObjectState { + return &NotificationState{NotificationManifest: n} +} + +// SortKey returns the sort key for this notification based on the sort mode. +func (n NotificationManifest) SortKey(sort string) string { + if sort == SortByPath { + return fmt.Sprintf("%02d_notification_%s", n.Level(), n.Path()) + } + return n.String() +} + +// GetRelations returns the relations for this notification. +func (n *NotificationManifest) GetRelations() Relations { + return n.Relations +} + +// SetRelations sets the relations for this notification. +func (n *NotificationManifest) SetRelations(relations Relations) { + n.Relations = relations +} + +// AddRelation adds a relation to this notification. +func (n *NotificationManifest) AddRelation(relation Relation) { + n.Relations = append(n.Relations, relation) +} diff --git a/internal/pkg/model/notificationstate.go b/internal/pkg/model/notificationstate.go new file mode 100644 index 0000000000..f7fee361ac --- /dev/null +++ b/internal/pkg/model/notificationstate.go @@ -0,0 +1,140 @@ +package model + +import ( + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +// NotificationState wraps notification manifest and local/remote states. +type NotificationState struct { + *NotificationManifest + Remote *Notification + Local *Notification + Ignore bool +} + +// ObjectName returns the subscription ID as the object name. +func (n *NotificationState) ObjectName() string { + return string(n.ID) +} + +// SetManifest sets the manifest for this notification state. +func (n *NotificationState) SetManifest(record ObjectManifest) { + n.NotificationManifest = record.(*NotificationManifest) +} + +// Manifest returns the manifest for this notification state. +func (n *NotificationState) Manifest() ObjectManifest { + return n.NotificationManifest +} + +// GetState returns the object for the specified state type. +func (n *NotificationState) GetState(stateType StateType) Object { + switch stateType { + case StateTypeLocal: + return n.Local + case StateTypeRemote: + return n.Remote + default: + panic(errors.Errorf(`unexpected state type "%T"`, stateType)) + } +} + +// HasState returns true if the notification has the specified state type. +func (n *NotificationState) HasState(stateType StateType) bool { + switch stateType { + case StateTypeLocal: + return n.Local != nil + case StateTypeRemote: + return n.Remote != nil + default: + panic(errors.Errorf(`unexpected state type "%T"`, stateType)) + } +} + +// HasManifest returns true if the notification has a manifest. +func (n *NotificationState) HasManifest() bool { + return n.NotificationManifest != nil +} + +// HasLocalState returns true if the notification has local state. +func (n *NotificationState) HasLocalState() bool { + return n.Local != nil +} + +// HasRemoteState returns true if the notification has remote state. +func (n *NotificationState) HasRemoteState() bool { + return n.Remote != nil +} + +// IsIgnored returns true if the notification is ignored. +func (n *NotificationState) IsIgnored() bool { + return n.Ignore +} + +// SetLocalState sets the local state for the notification. +func (n *NotificationState) SetLocalState(object Object) { + if object == nil { + n.Local = nil + } else { + n.Local = object.(*Notification) + } +} + +// SetRemoteState sets the remote state for the notification. +func (n *NotificationState) SetRemoteState(object Object) { + if object == nil { + n.Remote = nil + } else { + n.Remote = object.(*Notification) + } +} + +// LocalState returns the local notification object. +func (n *NotificationState) LocalState() Object { + if n.Local == nil { + return nil + } + return n.Local +} + +// RemoteState returns the remote notification object. +func (n *NotificationState) RemoteState() Object { + if n.Remote == nil { + return nil + } + return n.Remote +} + +// GetRelativePath returns the relative path to the notification directory. +func (n *NotificationState) GetRelativePath() string { + return string(n.RelativePath) +} + +// GetAbsPath returns the absolute path to the notification directory. +func (n *NotificationState) GetAbsPath() AbsPath { + return n.AbsPath +} + +// LocalOrRemoteState returns the local state if present, otherwise remote state. +func (n *NotificationState) LocalOrRemoteState() Object { + switch { + case n.HasLocalState(): + return n.LocalState() + case n.HasRemoteState(): + return n.RemoteState() + default: + panic(errors.New("object Local or Remote state must be set")) + } +} + +// RemoteOrLocalState returns the remote state if present, otherwise local state. +func (n *NotificationState) RemoteOrLocalState() Object { + switch { + case n.HasRemoteState(): + return n.RemoteState() + case n.HasLocalState(): + return n.LocalState() + default: + panic(errors.New("object Remote or Local state must be set")) + } +} diff --git a/internal/pkg/model/object.go b/internal/pkg/model/object.go index dcf35361fd..8cc44a1d5f 100644 --- a/internal/pkg/model/object.go +++ b/internal/pkg/model/object.go @@ -86,6 +86,8 @@ type ObjectStates interface { ConfigsFrom(branch BranchKey) (configs []*ConfigState) ConfigRows() []*ConfigRowState ConfigRowsFrom(config ConfigKey) (rows []*ConfigRowState) + Notifications() []*NotificationState + NotificationsFrom(config ConfigKey) (notifications []*NotificationState) Get(key Key) (ObjectState, bool) GetOrNil(key Key) ObjectState MustGet(key Key) ObjectState @@ -105,6 +107,7 @@ type Objects interface { ConfigsFrom(branch BranchKey) (configs []*Config) ConfigsWithRowsFrom(branch BranchKey) (configs []*ConfigWithRows) ConfigRowsFrom(config ConfigKey) (rows []*ConfigRow) + NotificationsFrom(config ConfigKey) (notifications []*Notification) } // Kind - type of the object, branch, config ... diff --git a/internal/pkg/naming/generator.go b/internal/pkg/naming/generator.go index e92519d54b..3b46624327 100644 --- a/internal/pkg/naming/generator.go +++ b/internal/pkg/naming/generator.go @@ -186,6 +186,17 @@ func (g Generator) ConfigRowPath(parentPath string, component *keboola.Component return g.registry.ensureUniquePath(row.Key(), p) } +func (g Generator) NotificationPath(parentPath string, notification *model.Notification) model.AbsPath { + if len(parentPath) == 0 { + panic(errors.Errorf(`notification "%s" parent path cannot be empty`, notification)) + } + + p := model.AbsPath{} + p.SetParentPath(parentPath) + p.SetRelativePath(fmt.Sprintf("notifications/sub-%s", notification.ID)) + return g.registry.ensureUniquePath(notification.Key(), p) +} + func (g Generator) BlocksDir(configDir string) string { return filesystem.Join(configDir, blocksDir) } diff --git a/internal/pkg/naming/generator_test.go b/internal/pkg/naming/generator_test.go index dcf463164b..5f4cb537f5 100644 --- a/internal/pkg/naming/generator_test.go +++ b/internal/pkg/naming/generator_test.go @@ -10,6 +10,20 @@ import ( . "github.com/keboola/keboola-as-code/internal/pkg/model" ) +func TestNotificationPath(t *testing.T) { + t.Parallel() + g := NewGenerator(TemplateWithIds(), NewRegistry()) + notification := &Notification{ + NotificationKey: NotificationKey{ + BranchID: 123, + ComponentID: "ex-generic-v2", + ConfigID: "my-config", + ID: "abc123", + }, + } + assert.Equal(t, "my-config-path/notifications/sub-abc123", g.NotificationPath("my-config-path", notification).Path()) +} + func TestUniquePathSameObjectType(t *testing.T) { t.Parallel() g := NewGenerator(TemplateWithIds(), NewRegistry()) diff --git a/internal/pkg/naming/matcher.go b/internal/pkg/naming/matcher.go index 5af8c79459..d995309f50 100644 --- a/internal/pkg/naming/matcher.go +++ b/internal/pkg/naming/matcher.go @@ -1,6 +1,8 @@ package naming import ( + "strings" + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" "github.com/keboola/keboola-as-code/internal/pkg/model" @@ -56,6 +58,11 @@ func (m PathMatcher) MatchConfigPath(parentKey model.Key, path model.AbsPath) (c return "", nil } +// MatchNotificationPath returns true if the path is a notification directory. +func (m PathMatcher) MatchNotificationPath(path model.AbsPath) bool { + return strings.HasPrefix(path.GetRelativePath(), "notifications/") +} + func (m PathMatcher) MatchConfigRowPath(component *keboola.Component, path model.AbsPath) bool { // Shared code if component.IsSharedCode() { diff --git a/internal/pkg/plan/persist/executor.go b/internal/pkg/plan/persist/executor.go index 99034ac1c0..12c7e70ad4 100644 --- a/internal/pkg/plan/persist/executor.go +++ b/internal/pkg/plan/persist/executor.go @@ -5,6 +5,7 @@ import ( "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + "github.com/keboola/keboola-as-code/internal/pkg/idgenerator" "github.com/keboola/keboola-as-code/internal/pkg/log" "github.com/keboola/keboola-as-code/internal/pkg/model" "github.com/keboola/keboola-as-code/internal/pkg/state" @@ -68,6 +69,11 @@ func (e *executor) persistNewObject(action *newObjectAction) { case model.ConfigRowKey: k.ID = keboola.RowID(newID) key = k + case model.NotificationKey: + // Assign a short random local ID to ensure unique manifest key. + // The real ID is assigned by the API on push (see buildNotificationCreateRequest success callback). + k.ID = keboola.NotificationSubscriptionID(idgenerator.Random(6)) + key = k default: panic(errors.Errorf(`unexpected type "%s" of the persisted object "%s"`, key.Kind(), key.Desc())) } diff --git a/internal/pkg/plan/persist/persist.go b/internal/pkg/plan/persist/persist.go index 2a711bcb6b..8a51ce4b8a 100644 --- a/internal/pkg/plan/persist/persist.go +++ b/internal/pkg/plan/persist/persist.go @@ -84,6 +84,9 @@ func (b *persistPlanBuilder) tryAdd(ctx context.Context, fullPath string, parent return true } case *model.ConfigState: + if b.tryAddNotification(ctx, path, parent.ConfigKey) != nil { + return true + } if b.tryAddConfigRow(ctx, path, parent.ConfigKey) != nil { return true } @@ -150,6 +153,21 @@ func (b *persistPlanBuilder) tryAddConfig(ctx context.Context, path model.AbsPat return action } +func (b *persistPlanBuilder) tryAddNotification(ctx context.Context, path model.AbsPath, parentKey model.ConfigKey) *newObjectAction { + if !b.PathMatcher().MatchNotificationPath(path) { + return nil + } + + notificationKey := model.NotificationKey{ + BranchID: parentKey.BranchID, + ComponentID: parentKey.ComponentID, + ConfigID: parentKey.ID, + } + action := &newObjectAction{AbsPath: path, Key: notificationKey, ParentKey: parentKey} + b.addAction(ctx, action) + return action +} + func (b *persistPlanBuilder) tryAddConfigRow(ctx context.Context, path model.AbsPath, parentKey model.ConfigKey) *newObjectAction { component, err := b.State.Components().GetOrErr(parentKey.ComponentID) if err != nil { diff --git a/internal/pkg/plan/push/push.go b/internal/pkg/plan/push/push.go index 086a798d22..74fd965dea 100644 --- a/internal/pkg/plan/push/push.go +++ b/internal/pkg/plan/push/push.go @@ -50,7 +50,14 @@ func parentExists(objectState model.ObjectState, objects model.ObjectStates) boo config, configFound := objects.Get(row.ConfigKey()) branch, branchFound := objects.Get(row.BranchKey()) return configFound && config.HasLocalState() && branchFound && branch.HasLocalState() - + case *model.NotificationState: + if v.Remote == nil { + return false + } + notification := v.Remote + config, configFound := objects.Get(notification.ConfigKey()) + branch, branchFound := objects.Get(notification.ConfigKey().BranchKey()) + return configFound && config.HasLocalState() && branchFound && branch.HasLocalState() default: panic(errors.Errorf(`unexpected type "%T"`, objectState)) } diff --git a/internal/pkg/project/manifest/file.go b/internal/pkg/project/manifest/file.go index e8aaf5d1c0..03b5d3fd39 100644 --- a/internal/pkg/project/manifest/file.go +++ b/internal/pkg/project/manifest/file.go @@ -2,6 +2,7 @@ package manifest import ( "context" + "fmt" "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" @@ -116,6 +117,18 @@ func (c *file) records() []model.ObjectManifest { row.ConfigID = config.ID out = append(out, row) } + for _, notification := range config.Notifications { + if notification != nil { + notification.BranchID = config.BranchID + notification.ComponentID = config.ComponentID + notification.ConfigID = config.ID + // Set path from ID if not stored in manifest (backwards compatibility) + if notification.GetRelativePath() == "" && notification.ID != "" { + notification.SetRelativePath(fmt.Sprintf("notifications/sub-%s", notification.ID)) + } + out = append(out, notification) + } + } } return out } @@ -150,6 +163,8 @@ func (c *file) setRecords(records []model.ObjectManifest) { ConfigManifest: *v, Rows: make([]*model.ConfigRowManifest, 0), } + // Reset notifications - they are populated below when NotificationManifest records are processed + config.Notifications = make([]*model.NotificationManifest, 0) configsMap[config.String()] = config c.Configs = append(c.Configs, config) } @@ -158,6 +173,11 @@ func (c *file) setRecords(records []model.ObjectManifest) { if found { config.Rows = append(config.Rows, v) } + case *model.NotificationManifest: + config, found := configsMap[v.ConfigKey().String()] + if found { + config.Notifications = append(config.Notifications, v) + } default: panic(errors.Errorf(`unexpected type "%T"`, manifest)) } diff --git a/internal/pkg/project/mappers.go b/internal/pkg/project/mappers.go index e8cd53a3fc..20822167d8 100644 --- a/internal/pkg/project/mappers.go +++ b/internal/pkg/project/mappers.go @@ -10,6 +10,7 @@ import ( "github.com/keboola/keboola-as-code/internal/pkg/mapper/defaultbucket" "github.com/keboola/keboola-as-code/internal/pkg/mapper/description" "github.com/keboola/keboola-as-code/internal/pkg/mapper/ignore" + "github.com/keboola/keboola-as-code/internal/pkg/mapper/notification" "github.com/keboola/keboola-as-code/internal/pkg/mapper/orchestrator" "github.com/keboola/keboola-as-code/internal/pkg/mapper/relations" "github.com/keboola/keboola-as-code/internal/pkg/mapper/scheduler" @@ -48,5 +49,7 @@ func MappersFor(s *state.State, d dependencies) (mapper.Mappers, error) { branchmetadata.NewMapper(s, d), // Configurations metadata configmetadata.NewMapper(s, d), + // Notifications + notification.NewMapper(s), }, nil } diff --git a/internal/pkg/service/cli/CLI_CONTEXT.md b/internal/pkg/service/cli/CLI_CONTEXT.md index 12d1bde785..fc3ecd8da8 100644 --- a/internal/pkg/service/cli/CLI_CONTEXT.md +++ b/internal/pkg/service/cli/CLI_CONTEXT.md @@ -2,6 +2,23 @@ This document provides architectural context for the Keboola CLI service (`kbc`) located in `/internal/pkg/service/cli/`. +## Notification Subscriptions + +The CLI supports managing notification subscriptions for configurations. Notifications are stored in `{config}/notifications/{subscription-id}/config.json` files and tracked in the manifest. + +**Key Features:** +- Config-level notifications (branch-level not yet supported) +- Events: job-failed, job-succeeded, job-warning, job-processing-long +- Channels: email or webhook +- Filters: Fine-grained control with operators (==, !=, >, <, >=, <=) +- Full pull/push/sync support + +**Operations:** +- `pull`: Load notifications from API → save to local files +- `push`: Create/delete notifications (no update API - delete + create) +- Manifest tracks subscription IDs for sync + + ## Overview The CLI provides a command-line interface for managing Keboola projects as code. It enables: diff --git a/internal/pkg/service/stream/storage/level/local/diskwriter/network/transport/protocol_kcp_test.go b/internal/pkg/service/stream/storage/level/local/diskwriter/network/transport/protocol_kcp_test.go index 14566edca7..35753cc00e 100644 --- a/internal/pkg/service/stream/storage/level/local/diskwriter/network/transport/protocol_kcp_test.go +++ b/internal/pkg/service/stream/storage/level/local/diskwriter/network/transport/protocol_kcp_test.go @@ -17,7 +17,7 @@ func TestTransport_SmallData_KCP(t *testing.T) { func TestTransportBiggerData_KCP(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { - t.Skip("skipped on Windows") + t.Skip("unstable on Windows: KCP data corruption under high UDP throughput") } testTransportBiggerData(t, func(cfg network.Config) transport.Protocol { return kcp.New(cfg) }) } diff --git a/internal/pkg/state/local/paths.go b/internal/pkg/state/local/paths.go index c13d2bea85..a4f4e54757 100644 --- a/internal/pkg/state/local/paths.go +++ b/internal/pkg/state/local/paths.go @@ -109,6 +109,12 @@ func (g *PathsGenerator) doUpdate(objectState model.ObjectState, origin model.Ke } } + // Notifications use user-defined folder names; never auto-rename them. + if _, ok := objectState.(*model.NotificationState); ok && objectState.GetRelativePath() != "" { + g.processed[objectState.Key().String()] = true + return nil + } + // Re-generate object path IF rename is enabled OR path is not set if objectState.GetRelativePath() == "" || g.rename { if err := g.regeneratePath(objectState, object, oldPath, renameFrom); err != nil { @@ -140,6 +146,9 @@ func (g *PathsGenerator) regeneratePath(objectState model.ObjectState, object mo } else { return err } + case *model.NotificationState: + notification := object.(*model.Notification) + v.AbsPath = g.NamingGenerator().NotificationPath(v.GetParentPath(), notification) default: panic(errors.Errorf(`unexpect type "%T"`, objectState)) } diff --git a/internal/pkg/state/manifest/records.go b/internal/pkg/state/manifest/records.go index 2b683f25b4..246975ed44 100644 --- a/internal/pkg/state/manifest/records.go +++ b/internal/pkg/state/manifest/records.go @@ -149,6 +149,8 @@ func (r *Records) CreateOrGetRecord(key model.Key) (manifest model.ObjectManifes manifest = &model.ConfigManifest{ConfigKey: v} case model.ConfigRowKey: manifest = &model.ConfigRowManifest{ConfigRowKey: v} + case model.NotificationKey: + manifest = &model.NotificationManifest{NotificationKey: v} default: panic(errors.Errorf("unexpected type \"%s\"", key)) } @@ -195,7 +197,18 @@ func (r *Records) PersistRecord(record model.ObjectManifest) error { // Mark persisted -> will be saved to manifest.json record.State().SetPersisted() - r.all.Set(record.Key().String(), record) + // Remove any stale entry stored under a different key that points to the same record. + // This handles re-keying: when a manifest's ID changes (e.g., a notification gets its + // API-assigned ID after push), the old entry must be removed before adding the new one. + newKeyStr := record.Key().String() + for _, k := range r.all.Keys() { + if v, _ := r.all.Get(k); v == record && k != newKeyStr { + r.all.Delete(k) + break + } + } + + r.all.Set(newKeyStr, record) r.changed = true return nil } diff --git a/internal/pkg/state/registry/proxy.go b/internal/pkg/state/registry/proxy.go index 3f59435531..09f1a2807e 100644 --- a/internal/pkg/state/registry/proxy.go +++ b/internal/pkg/state/registry/proxy.go @@ -91,3 +91,13 @@ func (f *Proxy) ConfigRowsFrom(config model.ConfigKey) (rows []*model.ConfigRow) } return out } + +func (f *Proxy) NotificationsFrom(config model.ConfigKey) (notifications []*model.Notification) { + var out []*model.Notification + for _, notification := range f.registry.NotificationsFrom(config) { + if notification.HasState(f.stateType) { + out = append(out, notification.GetState(f.stateType).(*model.Notification)) + } + } + return out +} diff --git a/internal/pkg/state/registry/registry.go b/internal/pkg/state/registry/registry.go index 95c96815df..f9b33ebd84 100644 --- a/internal/pkg/state/registry/registry.go +++ b/internal/pkg/state/registry/registry.go @@ -284,6 +284,27 @@ func (s *Registry) ConfigRowsFrom(config model.ConfigKey) (rows []*model.ConfigR return rows } +func (s *Registry) Notifications() (notifications []*model.NotificationState) { + for _, object := range s.All() { + if v, ok := object.(*model.NotificationState); ok { + notifications = append(notifications, v) + } + } + return notifications +} + +func (s *Registry) NotificationsFrom(config model.ConfigKey) (notifications []*model.NotificationState) { + for _, object := range s.All() { + if v, ok := object.(*model.NotificationState); ok { + if v.BranchID != config.BranchID || v.ComponentID != config.ComponentID || v.ConfigID != config.ID { + continue + } + notifications = append(notifications, v) + } + } + return notifications +} + func (s *Registry) GetPath(key model.Key) (model.AbsPath, bool) { objectState, found := s.Get(key) if !found { @@ -328,6 +349,16 @@ func (s *Registry) CreateFrom(manifest model.ObjectManifest) (model.ObjectState, return objectState, s.Set(objectState) } +// Set adds a NEW object to the registry. +// CRITICAL: This method ONLY works for NEW objects. If the object already exists, it returns an error. +// +// To update an existing object: +// +// existingState, _ := registry.Get(key) +// existingState.Local = newValue // Just assign - no Set() call needed +// +// Common mistake: Calling Set() after updating an existing object's fields. +// See docs/CLI_OBJECT_LIFECYCLE.md for details. func (s *Registry) Set(objectState model.ObjectState) error { s.lock.Lock() defer s.lock.Unlock() diff --git a/internal/pkg/state/remote/manager.go b/internal/pkg/state/remote/manager.go index 529032f392..ffd19a2fd2 100644 --- a/internal/pkg/state/remote/manager.go +++ b/internal/pkg/state/remote/manager.go @@ -187,6 +187,11 @@ func (u *UnitOfWork) LoadAll(filter model.ObjectsFilter) { } } } + + // Load notifications using manifest to determine parent configs + if err := u.loadNotifications(ctx); err != nil { + errs.Append(err) + } return errs.ErrorOrNil() }) @@ -234,6 +239,28 @@ func (u *UnitOfWork) SaveObject(objectState model.ObjectState, object model.Obje return } + // Special handling for notifications (no update API, only create/delete) + if notificationState, ok := objectState.(*model.NotificationState); ok { + notification := object.(*model.Notification) + level := object.Level() + if notificationState.HasRemoteState() && !changedFields.IsEmpty() { + // Modification: delete old notification, then create new one after successful deletion. + // The create must be sequenced after the delete (no native update API). + existingNotification := notificationState.RemoteState().(*model.Notification) + delReq := u.keboolaProjectAPI. + DeleteNotificationSubscriptionRequest(keboola.NotificationSubscriptionKey{ID: existingNotification.ID}). + WithOnSuccess(func(_ context.Context, _ request.NoResult) error { + u.runGroupFor(level).Add(u.buildNotificationCreateRequest(notificationState, notification)) + return nil + }) + u.runGroupFor(level).Add(delReq) + } else if !notificationState.HasRemoteState() { + // New notification: create directly. + u.runGroupFor(level).Add(u.buildNotificationCreateRequest(notificationState, notification)) + } + return + } + // Invoke mapper apiObject := deepcopy.Copy(object).(model.Object) recipe := model.NewRemoteSaveRecipe(objectState.Manifest(), apiObject, changedFields) @@ -254,6 +281,24 @@ func (u *UnitOfWork) DeleteObject(objectState model.ObjectState) { return } } + + // Special handling for notifications + if notificationState, ok := objectState.(*model.NotificationState); ok { + if notificationState.HasRemoteState() { + notification := notificationState.RemoteState().(*model.Notification) + req := u.keboolaProjectAPI. + DeleteNotificationSubscriptionRequest(keboola.NotificationSubscriptionKey{ID: notification.ID}). + WithOnSuccess(func(_ context.Context, _ request.NoResult) error { + u.Manifest().Delete(notificationState) + notificationState.SetRemoteState(nil) + u.changes.AddDeleted(notificationState) + return nil + }) + u.runGroupFor(objectState.Level()).Add(req) + } + return + } + u.delete(objectState) } @@ -290,6 +335,21 @@ func (u *UnitOfWork) Invoke() error { u.errors.Append(err) } + // Write auto-filters back to local notification files after create/update. + // The API response includes all filters (auto-populated + user-defined), + // which were stored in notification.Filters during the WithOnSuccess callback. + localWork := u.localManager.NewUnitOfWork(u.ctx) + for _, objectState := range u.changes.Created() { + if notificationState, ok := objectState.(*model.NotificationState); ok { + if notificationState.HasLocalState() { + localWork.SaveObject(notificationState, notificationState.LocalState(), model.NewChangedFields("filters")) + } + } + } + if err := localWork.Invoke(); err != nil { + u.errors.Append(err) + } + u.invoked = true return u.errors.ErrorOrNil() } diff --git a/internal/pkg/state/remote/manager_test.go b/internal/pkg/state/remote/manager_test.go index 4c6c91ae75..ff5e7dcf10 100644 --- a/internal/pkg/state/remote/manager_test.go +++ b/internal/pkg/state/remote/manager_test.go @@ -468,10 +468,17 @@ func newTestRemoteUOW(t *testing.T, mappers ...any) (*remote.UnitOfWork, *httpmo t.Helper() c, httpTransport := client.NewMockedClient() httpTransport.RegisterResponder(resty.MethodGet, `/v2/storage/?exclude=components`, - httpmock.NewStringResponder(200, `{ - "services": [], - "features": [] - }`), + httpmock.NewJsonResponderOrPanic(200, &keboola.Index{ + Services: keboola.Services{ + {ID: "notification", URL: "https://notification.mocked.transport.http"}, + }, + Features: keboola.Features{}, + }), + ) + httpTransport.RegisterResponder( + resty.MethodGet, + `https://notification.mocked.transport.http/project-subscriptions`, + httpmock.NewJsonResponderOrPanic(200, []*keboola.NotificationSubscription{}), ) api, err := keboola.NewAuthorizedAPI(t.Context(), "https://connection.keboola.com", "my-token", keboola.WithClient(&c)) diff --git a/internal/pkg/state/remote/notification.go b/internal/pkg/state/remote/notification.go new file mode 100644 index 0000000000..886dde2b2b --- /dev/null +++ b/internal/pkg/state/remote/notification.go @@ -0,0 +1,194 @@ +package remote + +import ( + "context" + "fmt" + "time" + + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" + "github.com/keboola/keboola-sdk-go/v2/pkg/request" + + "github.com/keboola/keboola-as-code/internal/pkg/model" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +// loadNotifications loads notification subscriptions from API and matches them to configs. +// For each notification, we determine the parent config by matching filters (job.configuration.id). +func (u *UnitOfWork) loadNotifications(ctx context.Context) error { + // Load all notification subscriptions from API + subscriptions, err := u.keboolaProjectAPI. + ListNotificationSubscriptionsRequest(). + Send(ctx) + if err != nil { + return err + } + + // Build map of "branchID/configID" -> config manifest for collision-safe lookup + configsByKey := make(map[string]*model.ConfigManifest) + for _, record := range u.Manifest().All() { + if configManifest, ok := record.(*model.ConfigManifest); ok { + key := fmt.Sprintf("%d/%s", configManifest.BranchID, configManifest.ID) + configsByKey[key] = configManifest + } + } + + // Load each notification from API + for _, apiSub := range *subscriptions { + // Collect branch.id and job.configuration.id from equality filters + var branchIDStr, configIDStr string + for _, filter := range apiSub.Filters { + if filter.Operator != keboola.NotificationFilterOperatorEquals { + continue + } + switch filter.Field { + case "branch.id": + branchIDStr = filter.Value + case "job.configuration.id": + configIDStr = filter.Value + } + } + + // Skip if no config ID filter present + if configIDStr == "" { + continue + } + + // Look up config using composite key to avoid branch collisions + var parentConfig *model.ConfigManifest + if branchIDStr != "" { + parentConfig = configsByKey[branchIDStr+"/"+configIDStr] + } + + // Skip if we don't have this config locally + if parentConfig == nil { + continue + } + + // Create notification with parent config info + notification := model.NewNotification(apiSub) + notification.BranchID = parentConfig.BranchID + notification.ComponentID = parentConfig.ComponentID + notification.ConfigID = parentConfig.ID + + // Use loadObject pattern (same as config rows) to properly create manifest and state + if err := u.loadObject(notification); err != nil { + return err + } + + // Get the notification state that was just created + notificationState, found := u.state.Get(notification.Key()) + if !found { + continue + } + + // Set parent path on the manifest + notificationState.Manifest().SetParentPath(parentConfig.Path()) + } + + return nil +} + +// createNotificationRequest builds an API request to create a notification subscription. +// Auto-populates standard filters (branch.id, job.component.id, job.configuration.id) from parent context, +// then merges with user-specified filters. +func (u *UnitOfWork) createNotificationRequest(notification *model.Notification) *keboola.CreateNotificationSubscriptionRequestBuilder { + req := u.keboolaProjectAPI. + NewCreateNotificationSubscriptionRequest( + notification.Event, + notification.Recipient.Channel, + notification.Recipient.Address, + ) + + // Auto-populate standard filters from parent context (only non-empty values) + var autoFilters []keboola.NotificationFilter + + branchIDStr := notification.BranchID.String() + if branchIDStr != "" && branchIDStr != "0" { + autoFilters = append(autoFilters, keboola.NotificationFilter{ + Field: "branch.id", + Operator: keboola.NotificationFilterOperatorEquals, + Value: branchIDStr, + }) + } + + componentIDStr := string(notification.ComponentID) + if componentIDStr != "" { + autoFilters = append(autoFilters, keboola.NotificationFilter{ + Field: "job.component.id", + Operator: keboola.NotificationFilterOperatorEquals, + Value: componentIDStr, + }) + } + + configIDStr := string(notification.ConfigID) + if configIDStr != "" { + autoFilters = append(autoFilters, keboola.NotificationFilter{ + Field: "job.configuration.id", + Operator: keboola.NotificationFilterOperatorEquals, + Value: configIDStr, + }) + } + + // Merge auto-populated filters with user-specified filters + autoFilters = append(autoFilters, notification.Filters...) + + if len(autoFilters) > 0 { + req = req.WithFilters(autoFilters) + } + + if notification.ExpiresAt != nil { + expiresAtStr := notification.ExpiresAt.String() + if t, err := time.Parse(time.RFC3339, expiresAtStr); err == nil { + req = req.WithAbsoluteExpiration(t) + } else { + req = req.WithRelativeExpiration(expiresAtStr) + } + } + + return req +} + +// buildNotificationCreateRequest wraps createNotificationRequest with success/error callbacks +// that update the local state and registry after the API call completes. +func (u *UnitOfWork) buildNotificationCreateRequest( + notificationState *model.NotificationState, + notification *model.Notification, +) request.APIRequest[*keboola.NotificationSubscription] { + return u.createNotificationRequest(notification).Build(). + WithOnError(func(_ context.Context, err error) error { + var notifErr *keboola.NotificationError + if errors.As(err, ¬ifErr) && notifErr.StatusCode() == 400 { + return errors.Errorf( + `failed to create notification "%s": %w. `+ + `This may be caused by invalid filter field names. `+ + `Ensure filters use correct field names like "job.configuration.id", "branch.id", "job.component.id"`, + notification.ID, err, + ) + } + return err + }). + WithOnSuccess(func(_ context.Context, created *keboola.NotificationSubscription) error { + // Create remote notification from API response (has all auto-populated filters) + remoteNotification := model.NewNotification(created) + remoteNotification.BranchID = notification.BranchID + remoteNotification.ComponentID = notification.ComponentID + remoteNotification.ConfigID = notification.ConfigID + + // Update local notification with API-assigned ID and auto-populated filters + notification.ID = created.ID + notification.CreatedAt = created.CreatedAt + notification.Filters = created.Filters + + // Before updating the manifest ID, detach the old key (with empty ID) from the naming + // registry. Without this, the subsequent PersistRecord call would fail because the path + // is still registered under the old key, causing a path collision with the new key. + u.Manifest().NamingRegistry().Detach(notificationState.Key()) + + // Update manifest ID so it is persisted correctly after push + notificationState.ID = created.ID + + notificationState.SetRemoteState(remoteNotification) + u.changes.AddCreated(notificationState) + return nil + }) +} diff --git a/internal/pkg/utils/testproject/project.go b/internal/pkg/utils/testproject/project.go index 27e4c84664..4cbc18f6ee 100644 --- a/internal/pkg/utils/testproject/project.go +++ b/internal/pkg/utils/testproject/project.go @@ -42,6 +42,7 @@ type Project struct { stateFilePath string branchesByID map[keboola.BranchID]*keboola.Branch branchesByName map[string]*keboola.Branch + notificationIDs []keboola.NotificationSubscriptionID logFn func(format string, a ...any) } @@ -176,6 +177,7 @@ func (p *Project) Clean() error { p.defaultBranch = defaultBranch p.branchesByID = make(map[keboola.BranchID]*keboola.Branch) p.branchesByName = make(map[string]*keboola.Branch) + p.notificationIDs = nil p.logf("■ Cleanup done.") return nil @@ -255,6 +257,12 @@ func (p *Project) SetState(ctx context.Context, fs filesystem.Fs, projectStateFi if err != nil { return err } + + // Create notification subscriptions + err = p.createNotificationSubscriptions(stateFile.NotificationSubscriptions) + if err != nil { + return err + } } p.logf("■ Project state set.") @@ -494,6 +502,71 @@ func (p *Project) createSandboxes(defaultBranchID keboola.BranchID, sandboxes [] return nil } +func (p *Project) createNotificationSubscriptions(subscriptions []*keboola.NotificationSubscription) error { + if len(subscriptions) == 0 { + return nil + } + + // Check if notification service is available + index, err := p.keboolaProjectAPI.IndexRequest().Send(context.Background()) + if err != nil { + return errors.Errorf("cannot get Storage API index: %w", err) + } + + servicesMap := index.Services.ToMap() + if _, found := servicesMap.URLByID("notification"); !found { + p.logf("⚠️ Notification service not available, skipping %d notification subscription(s)...", len(subscriptions)) + return nil + } + + ctx, cancel := context.WithTimeoutCause(context.Background(), 5*time.Minute, errors.New("notification subscriptions creation timeout")) + defer cancel() + + wg := &sync.WaitGroup{} + errs := errors.NewMultiError() + + for _, subscription := range subscriptions { + sub := subscription + wg.Go(func() { + p.logf("▶ Notification subscription \"%s\"...", sub.ID) + + // Process filter values - replace environment variable placeholders + processedFilters := make([]keboola.NotificationFilter, len(sub.Filters)) + for i, filter := range sub.Filters { + processedFilters[i] = keboola.NotificationFilter{ + Field: filter.Field, + Operator: filter.Operator, + Value: testhelper.MustReplaceEnvsString(filter.Value, p.envs), + } + } + + req := p.keboolaProjectAPI. + NewCreateNotificationSubscriptionRequest(sub.Event, sub.Recipient.Channel, sub.Recipient.Address). + WithFilters(processedFilters) + + created, err := req.Send(ctx) + if err != nil { + errs.Append(errors.Errorf("could not create notification subscription \"%s\": %w", sub.ID, err)) + return + } + p.logf("✔️ Notification subscription \"%s\".", created.ID) + p.setEnv(fmt.Sprintf("TEST_NOTIFICATION_%s_ID", sub.ID), string(created.ID)) + + // Track notification ID for cleanup + p.mapsLock.Lock() + p.notificationIDs = append(p.notificationIDs, created.ID) + p.mapsLock.Unlock() + }) + } + + wg.Wait() + if errs.Len() > 0 { + return errs + } + + return nil +} + // createDefaultBranchRequest creates a request for the default branch. // The default branch already exists and cannot be created/deleted, so this only updates its description if needed. func (p *Project) createDefaultBranchRequest(fixture *fixtures.BranchState) request.APIRequest[*keboola.Branch] { diff --git a/internal/pkg/utils/testproject/snapshot.go b/internal/pkg/utils/testproject/snapshot.go index 05c2eddfc8..d8eeb549dc 100644 --- a/internal/pkg/utils/testproject/snapshot.go +++ b/internal/pkg/utils/testproject/snapshot.go @@ -194,6 +194,18 @@ func (p *Project) NewSnapshot() (*fixtures.ProjectSnapshot, error) { return req.SendOrErr(ctx) }) + // Notification subscriptions + var notifications []*keboola.NotificationSubscription + grp.Go(func() error { + subs, err := p.keboolaProjectAPI.ListNotificationSubscriptionsRequest().Send(ctx) + if err != nil { + // Silently skip if notification service is not available + return nil //nolint:nilerr + } + notifications = *subs + return nil + }) + // Storage Files var files []*keboola.File grp.Go(func() error { @@ -299,6 +311,12 @@ func (p *Project) NewSnapshot() (*fixtures.ProjectSnapshot, error) { } } + // Notification subscriptions + sort.Slice(notifications, func(i, j int) bool { + return string(notifications[i].ID) < string(notifications[j].ID) + }) + snapshot.NotificationSubscriptions = notifications + // Sort by name reflecthelper.SortByName(snapshot.Branches) for _, b := range snapshot.Branches { diff --git a/pkg/lib/operation/project/sync/push/operation.go b/pkg/lib/operation/project/sync/push/operation.go index 3ec8e401f5..b366a49340 100644 --- a/pkg/lib/operation/project/sync/push/operation.go +++ b/pkg/lib/operation/project/sync/push/operation.go @@ -14,6 +14,7 @@ import ( "github.com/keboola/keboola-as-code/internal/pkg/telemetry" "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" "github.com/keboola/keboola-as-code/pkg/lib/operation/project/local/encrypt" + saveManifest "github.com/keboola/keboola-as-code/pkg/lib/operation/project/local/manifest/save" "github.com/keboola/keboola-as-code/pkg/lib/operation/project/local/validate" createDiff "github.com/keboola/keboola-as-code/pkg/lib/operation/project/sync/diff/create" ) @@ -115,6 +116,11 @@ func Run(ctx context.Context, projectState *project.State, o Options, d dependen return err } + // Save manifest (e.g., notification IDs assigned by API after create) + if _, err := saveManifest.Run(ctx, projectState.ProjectManifest(), projectState.Fs(), d); err != nil { + return err + } + logger.Info(ctx, "Push done.") } return nil diff --git a/test/cli/pull/notifications-config-level/args b/test/cli/pull/notifications-config-level/args new file mode 100644 index 0000000000..ed1600c941 --- /dev/null +++ b/test/cli/pull/notifications-config-level/args @@ -0,0 +1 @@ +pull --storage-api-token=%%TEST_KBC_STORAGE_API_TOKEN%% --storage-api-host=%%TEST_KBC_STORAGE_API_HOST%% diff --git a/test/cli/pull/notifications-config-level/expected-code b/test/cli/pull/notifications-config-level/expected-code new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/test/cli/pull/notifications-config-level/expected-code @@ -0,0 +1 @@ +0 diff --git a/test/cli/pull/notifications-config-level/expected-stderr b/test/cli/pull/notifications-config-level/expected-stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/expected-stdout b/test/cli/pull/notifications-config-level/expected-stdout new file mode 100644 index 0000000000..421886d9ad --- /dev/null +++ b/test/cli/pull/notifications-config-level/expected-stdout @@ -0,0 +1,4 @@ +Plan for "pull" operation: + * C main/extractor/ex-generic-v2/empty | changed: description + + N main/extractor/ex-generic-v2/empty/notifications/sub-%%TEST_NOTIFICATION_SUB_123_ID%% +Pull done. diff --git a/test/cli/pull/notifications-config-level/in/.gitkeep b/test/cli/pull/notifications-config-level/in/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/in/.keboola/manifest.json b/test/cli/pull/notifications-config-level/in/.keboola/manifest.json new file mode 100644 index 0000000000..76b319e72b --- /dev/null +++ b/test/cli/pull/notifications-config-level/in/.keboola/manifest.json @@ -0,0 +1,47 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": ["__all__"], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/empty", + "rows": [] + } + ] +} diff --git a/test/cli/pull/notifications-config-level/in/description.md b/test/cli/pull/notifications-config-level/in/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/in/main/description.md b/test/cli/pull/notifications-config-level/in/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/config.json b/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/description.md b/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/meta.json b/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/pull/notifications-config-level/in/main/extractor/ex-generic-v2/empty/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/pull/notifications-config-level/in/main/meta.json b/test/cli/pull/notifications-config-level/in/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/pull/notifications-config-level/in/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/pull/notifications-config-level/initial-state.json b/test/cli/pull/notifications-config-level/initial-state.json new file mode 100644 index 0000000000..994d694440 --- /dev/null +++ b/test/cli/pull/notifications-config-level/initial-state.json @@ -0,0 +1,41 @@ +{ + "allBranchesConfigs": [], + "branches": [ + { + "branch": { + "name": "Main", + "isDefault": true + }, + "configs": [ + "empty" + ] + } + ], + "notificationSubscriptions": [ + { + "id": "sub-123", + "event": "job-failed", + "recipient": { + "channel": "email", + "address": "test@example.com" + }, + "filters": [ + { + "field": "branch.id", + "operator": "==", + "value": "%%TEST_BRANCH_MAIN_ID%%" + }, + { + "field": "job.component.id", + "operator": "==", + "value": "ex-generic-v2" + }, + { + "field": "job.configuration.id", + "operator": "==", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%" + } + ] + } + ] +} diff --git a/test/cli/pull/notifications-config-level/out/.gitkeep b/test/cli/pull/notifications-config-level/out/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/out/.keboola/manifest.json b/test/cli/pull/notifications-config-level/out/.keboola/manifest.json new file mode 100644 index 0000000000..e0f181bb51 --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/.keboola/manifest.json @@ -0,0 +1,55 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": [ + "__all__" + ], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/empty", + "rows": [], + "notifications": [ + { + "id": "%s", + "path": "notifications/sub-%s" + } + ] + } + ] +} diff --git a/test/cli/pull/notifications-config-level/out/.keboola/project.json b/test/cli/pull/notifications-config-level/out/.keboola/project.json new file mode 100644 index 0000000000..2f8539cf19 --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/.keboola/project.json @@ -0,0 +1,9 @@ +{ + "backends": [ + %A + ], + "features": [ + %A + ], + "defaultBranchId": %A +} diff --git a/test/cli/pull/notifications-config-level/out/description.md b/test/cli/pull/notifications-config-level/out/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/out/main/description.md b/test/cli/pull/notifications-config-level/out/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/config.json b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/description.md b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/description.md new file mode 100644 index 0000000000..28f6091c29 --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/description.md @@ -0,0 +1 @@ +test fixture diff --git a/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/meta.json b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/notifications/sub-123/config.json b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/notifications/sub-123/config.json new file mode 100644 index 0000000000..980164fb9e --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/notifications/sub-123/config.json @@ -0,0 +1,24 @@ +{ + "event": "job-failed", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + } + ], + "recipient": { + "channel": "email", + "address": "test@example.com" + } +} diff --git a/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/notifications/sub-123/meta.json b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/notifications/sub-123/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/main/extractor/ex-generic-v2/empty/notifications/sub-123/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/pull/notifications-config-level/out/main/meta.json b/test/cli/pull/notifications-config-level/out/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/pull/notifications-config-level/out/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/encrypt/out/.keboola/manifest.json b/test/cli/push/encrypt/out/.keboola/manifest.json index f2ce4b62f8..c002b65b20 100644 --- a/test/cli/push/encrypt/out/.keboola/manifest.json +++ b/test/cli/push/encrypt/out/.keboola/manifest.json @@ -9,7 +9,13 @@ "naming": { "branch": "{branch_name}", "config": "{component_type}/{component_id}/{config_name}", - "configRow": "rows/{config_row_name}" + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_id}-{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_id}-{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_id}-{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_id}-{config_name}" }, "allowedBranches": [ "__all__" diff --git a/test/cli/push/notifications-create-with-filters/args b/test/cli/push/notifications-create-with-filters/args new file mode 100644 index 0000000000..538cc6f1cf --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/args @@ -0,0 +1 @@ +push "add notification subscription" --force --storage-api-token %%TEST_KBC_STORAGE_API_TOKEN%% diff --git a/test/cli/push/notifications-create-with-filters/expected-code b/test/cli/push/notifications-create-with-filters/expected-code new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/expected-code @@ -0,0 +1 @@ +0 diff --git a/test/cli/push/notifications-create-with-filters/expected-state.json b/test/cli/push/notifications-create-with-filters/expected-state.json new file mode 100644 index 0000000000..c7f37ee7c3 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/expected-state.json @@ -0,0 +1,54 @@ +{ + "branches": [ + { + "branch": { + "name": "Main", + "description": "", + "isDefault": true + }, + "configs": [ + { + "componentId": "ex-generic-v2", + "name": "empty", + "description": "", + "changeDescription": "%A", + "configuration": {}, + "rows": [], + "isDisabled": false + } + ] + } + ], + "notificationSubscriptions": [ + { + "id": "%A", + "event": "job-processing-long", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + }, + { + "field": "durationOvertimePercentage", + "value": "0.75", + "operator": "\u003e=" + } + ], + "recipient": { + "channel": "email", + "address": "alerts@example.com" + } + } + ] +} diff --git a/test/cli/push/notifications-create-with-filters/expected-stderr b/test/cli/push/notifications-create-with-filters/expected-stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/expected-stdout b/test/cli/push/notifications-create-with-filters/expected-stdout new file mode 100644 index 0000000000..bfd225a4d0 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/expected-stdout @@ -0,0 +1,4 @@ +Plan for "push" operation: + * C main/extractor/ex-generic-v2/my-config | changed: description + + N main/extractor/ex-generic-v2/my-config/notifications/sub-alert +Push done. diff --git a/test/cli/push/notifications-create-with-filters/in/.gitkeep b/test/cli/push/notifications-create-with-filters/in/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/.gitkeep @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-create-with-filters/in/.keboola/manifest.json b/test/cli/push/notifications-create-with-filters/in/.keboola/manifest.json new file mode 100644 index 0000000000..67d71820ad --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/.keboola/manifest.json @@ -0,0 +1,53 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": ["__all__"], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [], + "notifications": [ + { + "id": "", + "path": "notifications/sub-alert" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-create-with-filters/in/description.md b/test/cli/push/notifications-create-with-filters/in/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/in/main/description.md b/test/cli/push/notifications-create-with-filters/in/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json new file mode 100644 index 0000000000..60c749cb82 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json @@ -0,0 +1,14 @@ +{ + "event": "job-processing-long", + "filters": [ + { + "field": "durationOvertimePercentage", + "operator": ">=", + "value": "0.75" + } + ], + "recipient": { + "channel": "email", + "address": "alerts@example.com" + } +} diff --git a/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create-with-filters/in/main/meta.json b/test/cli/push/notifications-create-with-filters/in/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/in/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-create-with-filters/initial-state.json b/test/cli/push/notifications-create-with-filters/initial-state.json new file mode 100644 index 0000000000..d93758ff3c --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/initial-state.json @@ -0,0 +1,12 @@ +{ + "allBranchesConfigs": [], + "branches": [ + { + "branch": { + "name": "Main", + "isDefault": true + }, + "configs": ["empty"] + } + ] +} diff --git a/test/cli/push/notifications-create-with-filters/out/.gitkeep b/test/cli/push/notifications-create-with-filters/out/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/.gitkeep @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-create-with-filters/out/.keboola/manifest.json b/test/cli/push/notifications-create-with-filters/out/.keboola/manifest.json new file mode 100644 index 0000000000..330fab9222 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/.keboola/manifest.json @@ -0,0 +1,55 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": [ + "__all__" + ], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [], + "notifications": [ + { + "id": "%A", + "path": "notifications/sub-alert" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-create-with-filters/out/description.md b/test/cli/push/notifications-create-with-filters/out/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/out/main/description.md b/test/cli/push/notifications-create-with-filters/out/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json new file mode 100644 index 0000000000..f431333e24 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json @@ -0,0 +1,29 @@ +{ + "event": "job-processing-long", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + }, + { + "field": "durationOvertimePercentage", + "value": "0.75", + "operator": "\u003e=" + } + ], + "recipient": { + "channel": "email", + "address": "alerts@example.com" + } +} diff --git a/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create-with-filters/out/main/meta.json b/test/cli/push/notifications-create-with-filters/out/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-create-with-filters/out/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-create/args b/test/cli/push/notifications-create/args new file mode 100644 index 0000000000..538cc6f1cf --- /dev/null +++ b/test/cli/push/notifications-create/args @@ -0,0 +1 @@ +push "add notification subscription" --force --storage-api-token %%TEST_KBC_STORAGE_API_TOKEN%% diff --git a/test/cli/push/notifications-create/expected-code b/test/cli/push/notifications-create/expected-code new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/test/cli/push/notifications-create/expected-code @@ -0,0 +1 @@ +0 diff --git a/test/cli/push/notifications-create/expected-state.json b/test/cli/push/notifications-create/expected-state.json new file mode 100644 index 0000000000..6e1fcdb7e3 --- /dev/null +++ b/test/cli/push/notifications-create/expected-state.json @@ -0,0 +1,49 @@ +{ + "branches": [ + { + "branch": { + "name": "Main", + "description": "", + "isDefault": true + }, + "configs": [ + { + "componentId": "ex-generic-v2", + "name": "empty", + "description": "", + "changeDescription": "%A", + "configuration": {}, + "rows": [], + "isDisabled": false + } + ] + } + ], + "notificationSubscriptions": [ + { + "id": "%A", + "event": "job-failed", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + } + ], + "recipient": { + "channel": "email", + "address": "alerts@example.com" + } + } + ] +} diff --git a/test/cli/push/notifications-create/expected-stderr b/test/cli/push/notifications-create/expected-stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/expected-stdout b/test/cli/push/notifications-create/expected-stdout new file mode 100644 index 0000000000..bfd225a4d0 --- /dev/null +++ b/test/cli/push/notifications-create/expected-stdout @@ -0,0 +1,4 @@ +Plan for "push" operation: + * C main/extractor/ex-generic-v2/my-config | changed: description + + N main/extractor/ex-generic-v2/my-config/notifications/sub-alert +Push done. diff --git a/test/cli/push/notifications-create/in/.gitkeep b/test/cli/push/notifications-create/in/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/in/.keboola/manifest.json b/test/cli/push/notifications-create/in/.keboola/manifest.json new file mode 100644 index 0000000000..67d71820ad --- /dev/null +++ b/test/cli/push/notifications-create/in/.keboola/manifest.json @@ -0,0 +1,53 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": ["__all__"], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [], + "notifications": [ + { + "id": "", + "path": "notifications/sub-alert" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-create/in/description.md b/test/cli/push/notifications-create/in/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/in/main/description.md b/test/cli/push/notifications-create/in/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json new file mode 100644 index 0000000000..97d4c1dbe8 --- /dev/null +++ b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json @@ -0,0 +1,7 @@ +{ + "event": "job-failed", + "recipient": { + "channel": "email", + "address": "alerts@example.com" + } +} diff --git a/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create/in/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create/in/main/meta.json b/test/cli/push/notifications-create/in/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-create/in/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-create/initial-state.json b/test/cli/push/notifications-create/initial-state.json new file mode 100644 index 0000000000..d93758ff3c --- /dev/null +++ b/test/cli/push/notifications-create/initial-state.json @@ -0,0 +1,12 @@ +{ + "allBranchesConfigs": [], + "branches": [ + { + "branch": { + "name": "Main", + "isDefault": true + }, + "configs": ["empty"] + } + ] +} diff --git a/test/cli/push/notifications-create/out/.gitkeep b/test/cli/push/notifications-create/out/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/out/.keboola/manifest.json b/test/cli/push/notifications-create/out/.keboola/manifest.json new file mode 100644 index 0000000000..330fab9222 --- /dev/null +++ b/test/cli/push/notifications-create/out/.keboola/manifest.json @@ -0,0 +1,55 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": [ + "__all__" + ], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [], + "notifications": [ + { + "id": "%A", + "path": "notifications/sub-alert" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-create/out/description.md b/test/cli/push/notifications-create/out/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/out/main/description.md b/test/cli/push/notifications-create/out/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json new file mode 100644 index 0000000000..766389d4e5 --- /dev/null +++ b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/config.json @@ -0,0 +1,24 @@ +{ + "event": "job-failed", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + } + ], + "recipient": { + "channel": "email", + "address": "alerts@example.com" + } +} diff --git a/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-create/out/main/extractor/ex-generic-v2/my-config/notifications/sub-alert/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-create/out/main/meta.json b/test/cli/push/notifications-create/out/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-create/out/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-delete/args b/test/cli/push/notifications-delete/args new file mode 100644 index 0000000000..08d7d136a7 --- /dev/null +++ b/test/cli/push/notifications-delete/args @@ -0,0 +1 @@ +push --force --storage-api-token %%TEST_KBC_STORAGE_API_TOKEN%% diff --git a/test/cli/push/notifications-delete/expected-code b/test/cli/push/notifications-delete/expected-code new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/test/cli/push/notifications-delete/expected-code @@ -0,0 +1 @@ +0 diff --git a/test/cli/push/notifications-delete/expected-state.json b/test/cli/push/notifications-delete/expected-state.json new file mode 100644 index 0000000000..d8a15a031d --- /dev/null +++ b/test/cli/push/notifications-delete/expected-state.json @@ -0,0 +1,22 @@ +{ + "branches": [ + { + "branch": { + "name": "Main", + "description": "", + "isDefault": true + }, + "configs": [ + { + "componentId": "ex-generic-v2", + "name": "empty", + "description": "", + "changeDescription": "%A", + "configuration": {}, + "rows": [], + "isDisabled": false + } + ] + } + ] +} diff --git a/test/cli/push/notifications-delete/expected-stderr b/test/cli/push/notifications-delete/expected-stderr new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/expected-stderr @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/expected-stdout b/test/cli/push/notifications-delete/expected-stdout new file mode 100644 index 0000000000..c351494b71 --- /dev/null +++ b/test/cli/push/notifications-delete/expected-stdout @@ -0,0 +1,4 @@ +Plan for "push" operation: + * C main/extractor/ex-generic-v2/my-config | changed: description + × N main/extractor/ex-generic-v2/my-config/notifications/sub-%A +Push done. diff --git a/test/cli/push/notifications-delete/in/.gitkeep b/test/cli/push/notifications-delete/in/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-delete/in/.keboola/manifest.json b/test/cli/push/notifications-delete/in/.keboola/manifest.json new file mode 100644 index 0000000000..5602bf7e6c --- /dev/null +++ b/test/cli/push/notifications-delete/in/.keboola/manifest.json @@ -0,0 +1,47 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": ["__all__"], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [] + } + ] +} diff --git a/test/cli/push/notifications-delete/in/description.md b/test/cli/push/notifications-delete/in/description.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/in/description.md @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/in/main/description.md b/test/cli/push/notifications-delete/in/main/description.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/in/main/description.md @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/description.md @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-delete/in/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-delete/in/main/meta.json b/test/cli/push/notifications-delete/in/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-delete/in/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-delete/initial-state.json b/test/cli/push/notifications-delete/initial-state.json new file mode 100644 index 0000000000..f228237502 --- /dev/null +++ b/test/cli/push/notifications-delete/initial-state.json @@ -0,0 +1,29 @@ +{ + "allBranchesConfigs": [], + "branches": [ + { + "branch": { + "name": "Main", + "isDefault": true + }, + "configs": ["empty"] + } + ], + "notificationSubscriptions": [ + { + "id": "existing-sub-123", + "event": "job-failed", + "recipient": { + "channel": "email", + "address": "test@example.com" + }, + "filters": [ + { + "field": "job.configuration.id", + "operator": "==", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-delete/out/.gitkeep b/test/cli/push/notifications-delete/out/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-delete/out/.keboola/manifest.json b/test/cli/push/notifications-delete/out/.keboola/manifest.json new file mode 100644 index 0000000000..5602bf7e6c --- /dev/null +++ b/test/cli/push/notifications-delete/out/.keboola/manifest.json @@ -0,0 +1,47 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": ["__all__"], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [] + } + ] +} diff --git a/test/cli/push/notifications-delete/out/description.md b/test/cli/push/notifications-delete/out/description.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/out/description.md @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/out/main/description.md b/test/cli/push/notifications-delete/out/main/description.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/out/main/description.md @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/description.md @@ -0,0 +1 @@ + diff --git a/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-delete/out/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-delete/out/main/meta.json b/test/cli/push/notifications-delete/out/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-delete/out/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-update/args b/test/cli/push/notifications-update/args new file mode 100644 index 0000000000..6bcc23e9d3 --- /dev/null +++ b/test/cli/push/notifications-update/args @@ -0,0 +1 @@ +push "update notification address" --force --storage-api-token %%TEST_KBC_STORAGE_API_TOKEN%% diff --git a/test/cli/push/notifications-update/expected-code b/test/cli/push/notifications-update/expected-code new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/test/cli/push/notifications-update/expected-code @@ -0,0 +1 @@ +0 diff --git a/test/cli/push/notifications-update/expected-state.json b/test/cli/push/notifications-update/expected-state.json new file mode 100644 index 0000000000..196c15ce34 --- /dev/null +++ b/test/cli/push/notifications-update/expected-state.json @@ -0,0 +1,49 @@ +{ + "branches": [ + { + "branch": { + "name": "Main", + "description": "", + "isDefault": true + }, + "configs": [ + { + "componentId": "ex-generic-v2", + "name": "empty", + "description": "", + "changeDescription": "%A", + "configuration": {}, + "rows": [], + "isDisabled": false + } + ] + } + ], + "notificationSubscriptions": [ + { + "id": "%A", + "event": "job-failed", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + } + ], + "recipient": { + "channel": "email", + "address": "new@example.com" + } + } + ] +} diff --git a/test/cli/push/notifications-update/expected-stderr b/test/cli/push/notifications-update/expected-stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/expected-stdout b/test/cli/push/notifications-update/expected-stdout new file mode 100644 index 0000000000..4a84927d08 --- /dev/null +++ b/test/cli/push/notifications-update/expected-stdout @@ -0,0 +1,4 @@ +Plan for "push" operation: + * C main/extractor/ex-generic-v2/my-config | changed: description + * N main/extractor/ex-generic-v2/my-config/notifications/sub-123 | changed: filters, recipient +Push done. diff --git a/test/cli/push/notifications-update/in/.gitkeep b/test/cli/push/notifications-update/in/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/in/.keboola/manifest.json b/test/cli/push/notifications-update/in/.keboola/manifest.json new file mode 100644 index 0000000000..1107d447bb --- /dev/null +++ b/test/cli/push/notifications-update/in/.keboola/manifest.json @@ -0,0 +1,53 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": ["__all__"], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [], + "notifications": [ + { + "id": "%%TEST_NOTIFICATION_EXISTING_SUB_123_ID%%", + "path": "notifications/sub-123" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-update/in/description.md b/test/cli/push/notifications-update/in/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/in/main/description.md b/test/cli/push/notifications-update/in/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/notifications/sub-123/config.json b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/notifications/sub-123/config.json new file mode 100644 index 0000000000..ddb0b24073 --- /dev/null +++ b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/notifications/sub-123/config.json @@ -0,0 +1,7 @@ +{ + "event": "job-failed", + "recipient": { + "channel": "email", + "address": "new@example.com" + } +} diff --git a/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/notifications/sub-123/meta.json b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/notifications/sub-123/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-update/in/main/extractor/ex-generic-v2/my-config/notifications/sub-123/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-update/in/main/meta.json b/test/cli/push/notifications-update/in/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-update/in/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +} diff --git a/test/cli/push/notifications-update/initial-state.json b/test/cli/push/notifications-update/initial-state.json new file mode 100644 index 0000000000..359e715fdc --- /dev/null +++ b/test/cli/push/notifications-update/initial-state.json @@ -0,0 +1,29 @@ +{ + "allBranchesConfigs": [], + "branches": [ + { + "branch": { + "name": "Main", + "isDefault": true + }, + "configs": ["empty"] + } + ], + "notificationSubscriptions": [ + { + "id": "existing-sub-123", + "event": "job-failed", + "recipient": { + "channel": "email", + "address": "old@example.com" + }, + "filters": [ + { + "field": "job.configuration.id", + "operator": "==", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-update/out/.gitkeep b/test/cli/push/notifications-update/out/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/out/.keboola/manifest.json b/test/cli/push/notifications-update/out/.keboola/manifest.json new file mode 100644 index 0000000000..27a34992df --- /dev/null +++ b/test/cli/push/notifications-update/out/.keboola/manifest.json @@ -0,0 +1,55 @@ +{ + "version": 2, + "project": { + "id": %%TEST_KBC_PROJECT_ID%%, + "apiHost": "%%TEST_KBC_STORAGE_API_HOST%%" + }, + "allowTargetEnv": false, + "sortBy": "path", + "naming": { + "branch": "{branch_name}", + "config": "{component_type}/{component_id}/{config_name}", + "configRow": "rows/{config_row_name}", + "schedulerConfig": "schedules/{config_name}", + "sharedCodeConfig": "_shared/{target_component_id}", + "sharedCodeConfigRow": "codes/{config_row_name}", + "variablesConfig": "variables", + "variablesValuesRow": "values/{config_row_name}", + "dataAppConfig": "app/{component_id}/{config_name}" + }, + "allowedBranches": [ + "__all__" + ], + "ignoredComponents": [], + "templates": { + "repositories": [ + { + "type": "git", + "name": "keboola", + "url": "https://github.com/keboola/keboola-as-code-templates.git", + "ref": "main" + } + ] + }, + "branches": [ + { + "id": %%TEST_BRANCH_MAIN_ID%%, + "path": "main" + } + ], + "configurations": [ + { + "branchId": %%TEST_BRANCH_MAIN_ID%%, + "componentId": "ex-generic-v2", + "id": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "path": "extractor/ex-generic-v2/my-config", + "rows": [], + "notifications": [ + { + "id": "%A", + "path": "notifications/sub-123" + } + ] + } + ] +} diff --git a/test/cli/push/notifications-update/out/description.md b/test/cli/push/notifications-update/out/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/out/main/description.md b/test/cli/push/notifications-update/out/main/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/config.json b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/config.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/description.md b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/description.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/meta.json b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/meta.json new file mode 100644 index 0000000000..eb2c70d865 --- /dev/null +++ b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/meta.json @@ -0,0 +1,4 @@ +{ + "name": "empty", + "isDisabled": false +} diff --git a/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/notifications/sub-123/config.json b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/notifications/sub-123/config.json new file mode 100644 index 0000000000..28646cf249 --- /dev/null +++ b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/notifications/sub-123/config.json @@ -0,0 +1,24 @@ +{ + "event": "job-failed", + "filters": [ + { + "field": "branch.id", + "value": "%%TEST_BRANCH_MAIN_ID%%", + "operator": "==" + }, + { + "field": "job.component.id", + "value": "ex-generic-v2", + "operator": "==" + }, + { + "field": "job.configuration.id", + "value": "%%TEST_BRANCH_MAIN_CONFIG_EMPTY_ID%%", + "operator": "==" + } + ], + "recipient": { + "channel": "email", + "address": "new@example.com" + } +} diff --git a/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/notifications/sub-123/meta.json b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/notifications/sub-123/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/cli/push/notifications-update/out/main/extractor/ex-generic-v2/my-config/notifications/sub-123/meta.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/push/notifications-update/out/main/meta.json b/test/cli/push/notifications-update/out/main/meta.json new file mode 100644 index 0000000000..154a3ce050 --- /dev/null +++ b/test/cli/push/notifications-update/out/main/meta.json @@ -0,0 +1,4 @@ +{ + "name": "Main", + "isDefault": true +}