Repository: keboola/keboola-as-code
Pull request #2271: Adjust metadata cleanup process and configurations
Author: login: hosekpeter
Created: 2025-03-06T13:31:13Z | Merged: 2025-03-07T15:07:29Z

Jira: [PSGO-1017](https://keboola.atlassian.net/browse/PSGO-1017)

**Changes:**
- Separated metadata cleanup into file-specific and job-specific processes.
- Introduced `fileCleanupInterval` and `jobCleanupInterval` in the metadata cleanup configuration.
- Updated metadata cleanup intervals to allow for more specific configurations.



[PSGO-1017]: https://keboola.atlassian.net/browse/PSGO-1017?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/internal/pkg/service/stream/config/config_test.go b/internal/pkg/service/stream/config/config_test.go
index b167f617a4..171b574c67 100644
--- a/internal/pkg/service/stream/config/config_test.go
+++ b/internal/pkg/service/stream/config/config_test.go
@@ -149,10 +149,10 @@ storage:
                 # Statistics L2 in-memory cache invalidation interval. Validation rules: required,minDuration=100ms,maxDuration=5s
                 interval: 1s
     metadataCleanup:
-        # Enable local storage metadata cleanup.
-        enabled: true
-        # Cleanup interval. Validation rules: required,minDuration=30s,maxDuration=24h
-        interval: 30s
+        # Enable local storage metadata cleanup for jobs.
+        enableJobCleanup: true
+        # Enable local storage metadata cleanup for files.
+        enableFileCleanup: true
         # How many files are deleted in parallel. Validation rules: required,min=1,max=500
         concurrency: 50
         # How many errors are tolerated before failing. Validation rules: required,min=0,max=100
@@ -161,6 +161,10 @@ storage:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 1h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 10m0s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=5s,maxDuration=10m
+        jobCleanupInterval: 30s
     diskCleanup:
         # Enable local storage disks cleanup.
         enabled: true
diff --git a/internal/pkg/service/stream/storage/metacleanup/config.go b/internal/pkg/service/stream/storage/metacleanup/config.go
index 0485af9e76..62b12425f0 100644
--- a/internal/pkg/service/stream/storage/metacleanup/config.go
+++ b/internal/pkg/service/stream/storage/metacleanup/config.go
@@ -3,21 +3,25 @@ package metacleanup
 import "time"
 
 type Config struct {
-	Enabled                bool          `configKey:"enabled"  configUsage:"Enable local storage metadata cleanup."`
-	Interval               time.Duration `configKey:"interval"  configUsage:"Cleanup interval." validate:"required,minDuration=30s,maxDuration=24h"`
+	EnableJobCleanup       bool          `configKey:"enableJobCleanup"  configUsage:"Enable local storage metadata cleanup for jobs."`
+	EnableFileCleanup      bool          `configKey:"enableFileCleanup"  configUsage:"Enable local storage metadata cleanup for files."`
 	Concurrency            int           `configKey:"concurrency"  configUsage:"How many files are deleted in parallel." validate:"required,min=1,max=500"`
 	ErrorTolerance         int           `configKey:"errorTolerance"  configUsage:"How many errors are tolerated before failing." validate:"required,min=0,max=100"`
 	ActiveFileExpiration   time.Duration `configKey:"activeFileExpiration"  configUsage:"Expiration interval of a file that has not yet been imported." validate:"required,minDuration=1h,maxDuration=720h,gtefield=ArchivedFileExpiration"` // maxDuration=30 days
 	ArchivedFileExpiration time.Duration `configKey:"archivedFileExpiration"  configUsage:"Expiration interval of a file that has already been imported." validate:"required,minDuration=15m,maxDuration=720h"`                              // maxDuration=30 days
+	FileCleanupInterval    time.Duration `configKey:"fileCleanupInterval"  configUsage:"Cleanup interval of a file." validate:"required,minDuration=30s,maxDuration=24h"`
+	JobCleanupInterval     time.Duration `configKey:"jobCleanupInterval"  configUsage:"Cleanup interval of a job that has already completed." validate:"required,minDuration=5s,maxDuration=10m"`
 }
 
 func NewConfig() Config {
 	return Config{
-		Enabled:                true,
-		Interval:               30 * time.Second,
+		EnableFileCleanup:      true,
+		EnableJobCleanup:       true,
 		Concurrency:            50,
 		ErrorTolerance:         10,
 		ActiveFileExpiration:   7 * 24 * time.Hour, // 7 days
 		ArchivedFileExpiration: 1 * time.Hour,
+		FileCleanupInterval:    10 * time.Minute,
+		JobCleanupInterval:     30 * time.Second,
 	}
 }
diff --git a/internal/pkg/service/stream/storage/metacleanup/metacleanup.go b/internal/pkg/service/stream/storage/metacleanup/metacleanup.go
index 8b548235cf..b3953b3f06 100644
--- a/internal/pkg/service/stream/storage/metacleanup/metacleanup.go
+++ b/internal/pkg/service/stream/storage/metacleanup/metacleanup.go
@@ -56,6 +56,15 @@ type Node struct {
 	keboolaBridgeRepository *keboolaBridgeRepo.Repository
 }
 
+// cleanupEntity represents a periodic cleanup task.
+type cleanupEntity struct {
+	name        string
+	interval    time.Duration
+	enabled     bool
+	cleanupFunc func(context.Context) error
+	logger      log.Logger
+}
+
 func Start(d dependencies, cfg Config) error {
 	n := &Node{
 		config:                  cfg,
@@ -76,10 +85,6 @@ func Start(d dependencies, cfg Config) error {
 	}
 
 	ctx := context.Background()
-	if !n.config.Enabled {
-		n.logger.Info(ctx, "local storage metadata cleanup is disabled")
-		return nil
-	}
 
 	// Graceful shutdown
 	ctx, cancel := context.WithCancelCause(ctx)
@@ -91,37 +96,50 @@ func Start(d dependencies, cfg Config) error {
 		n.logger.Info(ctx, "shutdown done")
 	})
 
-	// Start timer
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-
-		ticker := d.Clock().NewTicker(n.config.Interval)
-		defer ticker.Stop()
-
-		for {
-			if err := n.cleanMetadata(ctx); err != nil && !errors.Is(err, context.Canceled) {
-				n.logger.Errorf(ctx, `local storage metadata cleanup failed: %s`, err)
-			}
-
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.Chan():
-				continue
-			}
-		}
-	}()
+	// Define cleanup entities
+	entities := n.cleanupEntities()
+
+	// Start cleanup entities
+	for _, task := range entities {
+		wg.Add(1)
+		go n.runCleanupTask(ctx, wg, d.Clock(), task)
+	}
 
 	return nil
 }
 
+// runCleanupTask runs a cleanup task periodically.
+func (n *Node) runCleanupTask(ctx context.Context, wg *sync.WaitGroup, clock clockwork.Clock, entity cleanupEntity) {
+	defer wg.Done()
+
+	if !entity.enabled {
+		entity.logger.Infof(ctx, "local storage metadata %s cleanup is disabled", entity.name)
+		return
+	}
+
+	ticker := clock.NewTicker(entity.interval)
+	defer ticker.Stop()
+
+	for {
+		if err := entity.cleanupFunc(ctx); err != nil && !errors.Is(err, context.Canceled) {
+			entity.logger.Errorf(ctx, `local storage metadata %s cleanup failed: %s`, entity.name, err)
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-ticker.Chan():
+			continue
+		}
+	}
+}
+
 // cleanMetadata iterates all files and deletes the expired ones.
-func (n *Node) cleanMetadata(ctx context.Context) (err error) {
-	ctx, cancel := context.WithTimeoutCause(context.WithoutCancel(ctx), 5*time.Minute, errors.New("clean metadata timeout"))
+func (n *Node) cleanMetadataFiles(ctx context.Context) (err error) {
+	ctx, cancel := context.WithTimeoutCause(context.WithoutCancel(ctx), 5*time.Minute, errors.New("clean metadata files timeout"))
 	defer cancel()
 
-	ctx, span := n.telemetry.Tracer().Start(ctx, "keboola.go.stream.model.cleanup.metadata.cleanMetadata")
+	ctx, span := n.telemetry.Tracer().Start(ctx, "keboola.go.stream.model.cleanup.metadata.cleanMetadataFiles")
 	defer span.End(&err)
 
 	// Measure count of deleted files
@@ -132,15 +150,6 @@ func (n *Node) cleanMetadata(ctx context.Context) (err error) {
 		n.logger.With(attribute.Int64("deletedFilesCount", count)).Info(ctx, `deleted "<deletedFilesCount>" files`)
 	}()
 
-	// Measure count of deleted storage jobs
-	jobCounter := atomic.NewInt64(0)
-	defer func() {
-		count := jobCounter.Load()
-		span.SetAttributes(attribute.Int64("deletedJobsCount", count))
-		n.logger.With(attribute.Int64("deletedJobsCount", count)).Info(ctx, `deleted "<deletedJobsCount>" jobs`)
-	}()
-
-	// Delete files in parallel, but with limit
 	n.logger.Info(ctx, `deleting metadata of expired files`)
 	grp, ctx := errgroup.WithContext(ctx)
 	grp.SetLimit(n.config.Concurrency)
@@ -173,7 +182,30 @@ func (n *Node) cleanMetadata(ctx context.Context) (err error) {
 		return err
 	}
 
+	return grp.Wait()
+}
+
+func (n *Node) cleanMetadataJobs(ctx context.Context) (err error) {
+	ctx, cancel := context.WithTimeoutCause(context.WithoutCancel(ctx), 5*time.Minute, errors.New("clean metadata jobs timeout"))
+	defer cancel()
+
+	ctx, span := n.telemetry.Tracer().Start(ctx, "keboola.go.stream.model.cleanup.metadata.cleanMetadataJobs")
+	defer span.End(&err)
+
+	// Measure count of deleted storage jobs
+	jobCounter := atomic.NewInt64(0)
+	defer func() {
+		count := jobCounter.Load()
+		span.SetAttributes(attribute.Int64("deletedJobsCount", count))
+		n.logger.With(attribute.Int64("deletedJobsCount", count)).Info(ctx, `deleted "<deletedJobsCount>" jobs`)
+	}()
+
 	n.logger.Info(ctx, `deleting metadata of success jobs`)
+	grp, ctx := errgroup.WithContext(ctx)
+	grp.SetLimit(n.config.Concurrency)
+
+	var errCount atomic.Uint32
+
 	// Iterate all storage jobs
 	err = n.keboolaBridgeRepository.
 		Job().
@@ -273,3 +305,22 @@ func (n *Node) isFileExpired(file model.File, age time.Duration) bool {
 	// Other files have a longer expiration so there is time for retries.
 	return age >= n.config.ActiveFileExpiration
 }
+
+func (n *Node) cleanupEntities() []cleanupEntity {
+	return []cleanupEntity{
+		{
+			name:        "file",
+			interval:    n.config.FileCleanupInterval,
+			enabled:     n.config.EnableFileCleanup,
+			cleanupFunc: n.cleanMetadataFiles,
+			logger:      n.logger,
+		},
+		{
+			name:        "job",
+			interval:    n.config.JobCleanupInterval,
+			enabled:     n.config.EnableJobCleanup,
+			cleanupFunc: n.cleanMetadataJobs,
+			logger:      n.logger,
+		},
+	}
+}
diff --git a/internal/pkg/service/stream/storage/metacleanup/metacleanup_test.go b/internal/pkg/service/stream/storage/metacleanup/metacleanup_test.go
index e12ea2f9b7..fb4e9055bd 100644
--- a/internal/pkg/service/stream/storage/metacleanup/metacleanup_test.go
+++ b/internal/pkg/service/stream/storage/metacleanup/metacleanup_test.go
@@ -76,7 +76,7 @@ func TestMetadataCleanup(t *testing.T) {
 	importedFileExpiration := time.Hour    // the first call of the doCleanup triggers it
 	activeFileExpiration := 30 * time.Hour // the third call of the doCleanup triggers it
 	cfg := metacleanup.NewConfig()
-	cfg.Interval = cleanupInterval
+	cfg.FileCleanupInterval = cleanupInterval
 	cfg.ActiveFileExpiration = activeFileExpiration
 	cfg.ArchivedFileExpiration = importedFileExpiration
 
@@ -286,7 +286,7 @@ func TestMetadataProcessingJobCleanup(t *testing.T) {
 	importedFileExpiration := time.Hour    // the first call of the doCleanup triggers it
 	activeFileExpiration := 30 * time.Hour // the third call of the doCleanup triggers it
 	cfg := metacleanup.NewConfig()
-	cfg.Interval = cleanupInterval
+	cfg.JobCleanupInterval = cleanupInterval
 	cfg.ActiveFileExpiration = activeFileExpiration
 	cfg.ArchivedFileExpiration = importedFileExpiration
 
@@ -388,7 +388,7 @@ func TestMetadataNotFoundJobCleanup(t *testing.T) {
 	importedFileExpiration := time.Hour    // the first call of the doCleanup triggers it
 	activeFileExpiration := 30 * time.Hour // the third call of the doCleanup triggers it
 	cfg := metacleanup.NewConfig()
-	cfg.Interval = cleanupInterval
+	cfg.JobCleanupInterval = cleanupInterval
 	cfg.ActiveFileExpiration = activeFileExpiration
 	cfg.ArchivedFileExpiration = importedFileExpiration
 
@@ -598,7 +598,7 @@ func TestMetadataProcessingJobCleanupErrorTolerance(t *testing.T) {
 	cfg := metacleanup.NewConfig()
 	cfg.Concurrency = 2
 	cfg.ErrorTolerance = 3
-	cfg.Interval = cleanupInterval
+	cfg.JobCleanupInterval = cleanupInterval
 	cfg.ActiveFileExpiration = activeFileExpiration
 	cfg.ArchivedFileExpiration = importedFileExpiration
 
diff --git a/provisioning/stream/kubernetes/templates/config/config-map.yaml b/provisioning/stream/kubernetes/templates/config/config-map.yaml
index 0a0418060a..a7fc4d6a3c 100644
--- a/provisioning/stream/kubernetes/templates/config/config-map.yaml
+++ b/provisioning/stream/kubernetes/templates/config/config-map.yaml
@@ -133,10 +133,10 @@ data:
             # Statistics L2 in-memory cache invalidation interval. Validation rules: required,minDuration=100ms,maxDuration=5s
             interval: 1s
       metadataCleanup:
-        # Enable local storage metadata cleanup.
-        enabled: true
-        # Cleanup interval. Validation rules: required,minDuration=30s,maxDuration=24h
-        interval: 30s
+        # Enable local storage metadata cleanup for jobs.
+        enableJobCleanup: true
+        # Enable local storage metadata cleanup for files.
+        enableFileCleanup: true
         # How many files are deleted in parallel. Validation rules: required,min=1,max=500
         concurrency: 50
         # How many errors are tolerated before failing. Validation rules: required,min=0,max=100
@@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 10m0s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=5s,maxDuration=10m
+        jobCleanupInterval: 30s
       diskCleanup:
         # Enable local storage disks cleanup.
         enabled: true
diff --git a/test/stream/bridge/keboola/keboola_test.go b/test/stream/bridge/keboola/keboola_test.go
index 1f6a9eb745..873b635a5a 100644
--- a/test/stream/bridge/keboola/keboola_test.go
+++ b/test/stream/bridge/keboola/keboola_test.go
@@ -45,7 +45,7 @@ func TestKeboolaBridgeWorkflow(t *testing.T) { // nolint: paralleltest
 		cfg.Encryption.Provider = encryption.ProviderAES
 		cfg.Encryption.AES.SecretKey = secretKey
 		// Enable metadata cleanup for removing storage jobs
-		cfg.Storage.MetadataCleanup.Enabled = true
+		cfg.Storage.MetadataCleanup.EnableJobCleanup = true
 		// Disable unrelated workers
 		cfg.Storage.DiskCleanup.Enabled = false
 		cfg.API.Task.CleanupEnabled = false
@@ -79,7 +79,7 @@ func TestKeboolaBridgeWorkflow(t *testing.T) { // nolint: paralleltest
 		}
 
 		// Cleanup should be perfomed more frequently to remove already finished storage jobs
-		cfg.Storage.MetadataCleanup.Interval = 10 * time.Second
+		cfg.Storage.MetadataCleanup.FileCleanupInterval = 10 * time.Second
 	}
 
 	ts := setup(t)
@@ -332,7 +332,7 @@ func TestNetworkIssuesKeboolaBridgeWorkflow(t *testing.T) { // nolint: parallelt
 		cfg.Encryption.Provider = encryption.ProviderAES
 		cfg.Encryption.AES.SecretKey = secretKey
 		// Enable metadata cleanup for removing storage jobs
-		cfg.Storage.MetadataCleanup.Enabled = true
+		cfg.Storage.MetadataCleanup.EnableJobCleanup = true
 		// Disable unrelated workers
 		cfg.Storage.DiskCleanup.Enabled = false
 		cfg.API.Task.CleanupEnabled = false
@@ -366,7 +366,7 @@ func TestNetworkIssuesKeboolaBridgeWorkflow(t *testing.T) { // nolint: parallelt
 		}
 
 		// Cleanup should be perfomed more frequently to remove already finished storage jobs
-		cfg.Storage.MetadataCleanup.Interval = 10 * time.Second
+		cfg.Storage.MetadataCleanup.FileCleanupInterval = 10 * time.Second
 	}
 
 	ts := setup(t)


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## PR Overview

This PR refactors the metadata cleanup process by splitting it into file‐ and job‐specific routines and updating the related configuration and tests. Key changes include:
- Replacing the monolithic cleanup with separate tasks for files and jobs.
- Introducing distinct cleanup intervals via FileCleanupInterval and JobCleanupInterval.
- Updating YAML configurations and tests to support the new cleanup process.

### Reviewed Changes

| File | Description |
| ---- | ----------- |
| internal/pkg/service/stream/storage/metacleanup/metacleanup.go | Refactors cleanup into two tasks and updates the logging and function names accordingly. |
| provisioning/stream/kubernetes/templates/config/config-map.yaml | Updates configuration keys to enable file and job cleanup and adds new intervals. |
| internal/pkg/service/stream/storage/metacleanup/config.go | Replaces legacy Enabled/Interval config fields with new job/file cleanup settings. |
| test/stream/bridge/keboola/keboola_test.go | Adapts tests to reference the new job cleanup configuration. |
| internal/pkg/service/stream/config/config_test.go | Updates configuration tests to reflect new cleanup keys and intervals. |
| internal/pkg/service/stream/storage/metacleanup/metacleanup_test.go | Adjusts tests to use the new cleanup interval configurations for files and jobs. |

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**provisioning/stream/kubernetes/templates/config/config-map.yaml:312**
* Duplicate keys 'keboolaStack' and 'keboolaRevision' appear in the config map, which may lead to ambiguous or overridden configuration. Consider consolidating these definitions to avoid potential conflicts.
```
  keboolaStack: "$KEBOOLA_STACK"
```
**internal/pkg/service/stream/storage/metacleanup/metacleanup.go:150**
* [nitpick] The log message uses a literal placeholder "<deletedFilesCount>" rather than the actual count. Update the message to either interpolate the count value or provide a clearer descriptive message.
```
n.logger.With(attribute.Int64("deletedFilesCount", count)).Info(ctx, `deleted "<deletedFilesCount>" files`)
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T10:28:28Z
user: login: Matovidlo
body: Just unfortunate naming and 1 thing missing in the original Jira task, sorry about that :see_no_evil: 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:48:20Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:57:05Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:01:12Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:13:16Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:13:22Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:13:29Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:13:34Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:13:41Z
user: login: jachym-tousek-keboola
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T12:55:10Z
user: login: Matovidlo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:56:21Z
user: login: jachym-tousek-keboola
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T12:56:27Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:56:34Z
user: login: jachym-tousek-keboola
body: See the comments.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-07T12:58:58Z
user: login: jachym-tousek-keboola
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T12:59:35Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:00:14Z
user: login: jachym-tousek-keboola
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T13:01:26Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:03:49Z
user: login: jachym-tousek-keboola
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T13:08:06Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:10:37Z
user: login: jachym-tousek-keboola
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T13:15:19Z
user: login: Matovidlo
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:22:47Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:24:24Z
user: login: hosekpeter
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:24:32Z
user: login: jachym-tousek-keboola
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T15:06:40Z

Review comments:
diff_hunk: @@ -161,6 +161,10 @@ storage:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 1h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: internal/pkg/service/stream/config/config_test.go
user: login: Matovidlo
body: This could be set up again to 10 minutes. We probablly forgot to mention it, it was previous configuration :see_no_evil: 
created_at: 2025-03-07T11:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,37 +96,50 @@ func Start(d dependencies, cfg Config) error {
 		n.logger.Info(ctx, "shutdown done")
 	})
 
-	// Start timer
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-
-		ticker := d.Clock().NewTicker(n.config.Interval)
-		defer ticker.Stop()
-
-		for {
-			if err := n.cleanMetadata(ctx); err != nil && !errors.Is(err, context.Canceled) {
-				n.logger.Errorf(ctx, `local storage metadata cleanup failed: %s`, err)
-			}
-
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.Chan():
-				continue
-			}
-		}
-	}()
+	// Define cleanup tasks
path: internal/pkg/service/stream/storage/metacleanup/metacleanup.go
user: login: Matovidlo
body: This is little bit unfortunate naming because we have tasks in general in `common` folder and in templates. 
```suggestion
	// Define cleanup Entities
```
created_at: 2025-03-07T11:46:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,37 +96,50 @@ func Start(d dependencies, cfg Config) error {
 		n.logger.Info(ctx, "shutdown done")
 	})
 
-	// Start timer
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-
-		ticker := d.Clock().NewTicker(n.config.Interval)
-		defer ticker.Stop()
-
-		for {
-			if err := n.cleanMetadata(ctx); err != nil && !errors.Is(err, context.Canceled) {
-				n.logger.Errorf(ctx, `local storage metadata cleanup failed: %s`, err)
-			}
-
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.Chan():
-				continue
-			}
-		}
-	}()
+	// Define cleanup tasks
+	tasks := n.cleanupTasks()
path: internal/pkg/service/stream/storage/metacleanup/metacleanup.go
user: login: Matovidlo
body: ```suggestion
	tasks := n.cleanupEntities()
```
created_at: 2025-03-07T11:47:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +56,15 @@ type Node struct {
 	keboolaBridgeRepository *keboolaBridgeRepo.Repository
 }
 
+// cleanupTask represents a periodic cleanup task.
+type cleanupTask struct {
path: internal/pkg/service/stream/storage/metacleanup/metacleanup.go
user: login: Matovidlo
body: ```suggestion
type cleanupEntity struct {
```
created_at: 2025-03-07T11:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,3 +309,5 @@ data:
         keyVaultUrl: ""
         # Key name in the vault. Validation rules: required
         keyName: ""
+  keboolaStack: "$KEBOOLA_STACK"
+  keboolaRevision: "$RELEASE_ID"
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: Matovidlo
body: Why this is moved? Autoformatting?
created_at: 2025-03-07T11:47:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -161,6 +161,10 @@ storage:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 1h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: internal/pkg/service/stream/config/config_test.go
user: login: hosekpeter
body: just fileCleanupInterval?
created_at: 2025-03-07T11:57:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,3 +309,5 @@ data:
         keyVaultUrl: ""
         # Key name in the vault. Validation rules: required
         keyName: ""
+  keboolaStack: "$KEBOOLA_STACK"
+  keboolaRevision: "$RELEASE_ID"
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: I don't know. I'll move it back.
created_at: 2025-03-07T12:01:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,3 +309,5 @@ data:
         keyVaultUrl: ""
         # Key name in the vault. Validation rules: required
         keyName: ""
+  keboolaStack: "$KEBOOLA_STACK"
+  keboolaRevision: "$RELEASE_ID"
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: [b857193](https://github.com/keboola/keboola-as-code/pull/2271/commits/b85719354545fa074646cb50c57dd49f0406fda7)
created_at: 2025-03-07T12:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +56,15 @@ type Node struct {
 	keboolaBridgeRepository *keboolaBridgeRepo.Repository
 }
 
+// cleanupTask represents a periodic cleanup task.
+type cleanupTask struct {
path: internal/pkg/service/stream/storage/metacleanup/metacleanup.go
user: login: hosekpeter
body: [b857193](https://github.com/keboola/keboola-as-code/pull/2271/commits/b85719354545fa074646cb50c57dd49f0406fda7)
created_at: 2025-03-07T12:13:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,37 +96,50 @@ func Start(d dependencies, cfg Config) error {
 		n.logger.Info(ctx, "shutdown done")
 	})
 
-	// Start timer
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-
-		ticker := d.Clock().NewTicker(n.config.Interval)
-		defer ticker.Stop()
-
-		for {
-			if err := n.cleanMetadata(ctx); err != nil && !errors.Is(err, context.Canceled) {
-				n.logger.Errorf(ctx, `local storage metadata cleanup failed: %s`, err)
-			}
-
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.Chan():
-				continue
-			}
-		}
-	}()
+	// Define cleanup tasks
+	tasks := n.cleanupTasks()
path: internal/pkg/service/stream/storage/metacleanup/metacleanup.go
user: login: hosekpeter
body: [b857193](https://github.com/keboola/keboola-as-code/pull/2271/commits/b85719354545fa074646cb50c57dd49f0406fda7)
created_at: 2025-03-07T12:13:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,37 +96,50 @@ func Start(d dependencies, cfg Config) error {
 		n.logger.Info(ctx, "shutdown done")
 	})
 
-	// Start timer
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-
-		ticker := d.Clock().NewTicker(n.config.Interval)
-		defer ticker.Stop()
-
-		for {
-			if err := n.cleanMetadata(ctx); err != nil && !errors.Is(err, context.Canceled) {
-				n.logger.Errorf(ctx, `local storage metadata cleanup failed: %s`, err)
-			}
-
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.Chan():
-				continue
-			}
-		}
-	}()
+	// Define cleanup tasks
path: internal/pkg/service/stream/storage/metacleanup/metacleanup.go
user: login: hosekpeter
body: [b857193](https://github.com/keboola/keboola-as-code/pull/2271/commits/b85719354545fa074646cb50c57dd49f0406fda7)
created_at: 2025-03-07T12:13:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -161,6 +161,10 @@ storage:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 1h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: internal/pkg/service/stream/config/config_test.go
user: login: hosekpeter
body: [b857193](https://github.com/keboola/keboola-as-code/pull/2271/commits/b85719354545fa074646cb50c57dd49f0406fda7)
created_at: 2025-03-07T12:13:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: jachym-tousek-keboola
body: Default file clean up should be less often. What was the original value @Matovidlo before you added jobs?
created_at: 2025-03-07T12:55:10Z
author_association: MEMBER
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: Matovidlo
body: 10 minutes I already mentioned it and I think Peter has changed it already in some commit
created_at: 2025-03-07T12:56:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=30s,maxDuration=24h
+        jobCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: jachym-tousek-keboola
body: Job cleanup needs to happen somewhat often so the allowed maximum should be a few minutes, not 24 hours.
created_at: 2025-03-07T12:56:26Z
author_association: MEMBER
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: I set it up to 10 minutes
created_at: 2025-03-07T12:56:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: jachym-tousek-keboola
body: Then why does it say 30s here in the k8s template?
created_at: 2025-03-07T12:59:35Z
author_association: MEMBER
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: [8251c46](https://github.com/keboola/keboola-as-code/pull/2271/commits/8251c46098b821330554d0a0d419b143d3b2a745)

created_at: 2025-03-07T13:00:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: jachym-tousek-keboola
body: Yeah that commit changed it everywhere else but not here in the k8s template which I think means it would still be 30s on production.
created_at: 2025-03-07T13:01:25Z
author_association: MEMBER
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=30s,maxDuration=24h
+        jobCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: How often do you want to do this cleaning?

Do you agree if the maximum is 10 minutes?

@Matovidlo @jachym-tousek-keboola 
created_at: 2025-03-07T13:03:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=30s,maxDuration=24h
+        jobCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: jachym-tousek-keboola
body: Yeah, 10 mins is fine by me.
created_at: 2025-03-07T13:08:06Z
author_association: MEMBER
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=30s,maxDuration=24h
+        jobCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: what default value? Can I use 30 seconds?
created_at: 2025-03-07T13:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=30s,maxDuration=24h
+        jobCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: jachym-tousek-keboola
body: Yup, no complaints about the default!
created_at: 2025-03-07T13:15:18Z
author_association: MEMBER
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
+        # Cleanup interval of a job that has already completed. Validation rules: required,minDuration=30s,maxDuration=24h
+        jobCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: [8b22ddd](https://github.com/keboola/keboola-as-code/pull/2271/commits/8b22dddd600d07f372fb2f19457247994e3065cb)
created_at: 2025-03-07T13:24:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,6 +145,10 @@ data:
         activeFileExpiration: 168h0m0s
         # Expiration interval of a file that has already been imported. Validation rules: required,minDuration=15m,maxDuration=720h
         archivedFileExpiration: 6h0m0s
+        # Cleanup interval of a file. Validation rules: required,minDuration=30s,maxDuration=24h
+        fileCleanupInterval: 30s
path: provisioning/stream/kubernetes/templates/config/config-map.yaml
user: login: hosekpeter
body: [8b22ddd](https://github.com/keboola/keboola-as-code/pull/2271/commits/8b22dddd600d07f372fb2f19457247994e3065cb)
created_at: 2025-03-07T13:24:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-07T13:36:14Z
body: ### Stream Kubernetes Diff [CI]


Between `base` aff7efaae0421311ccd5b7cc7f7a8b59750abe55 :arrow_left:  `head` 8b22dddd600d07f372fb2f19457247994e3065cb.


<details>
<summary>Expand</summary>

```diff

--- /tmp/artifacts/test-k8s-state.old.json.processed.kv	2025-03-07 13:36:09.789773886 +0000
+++ /tmp/artifacts/test-k8s-state.new.json.processed.kv	2025-03-07 13:36:10.182777154 +0000
@@ -13 +13 @@
-<ConfigMap/stream-config>.data["config.yaml"] = "###############################################################################################...
+<ConfigMap/stream-config>.data["config.yaml"] = "###############################################################################################...
@@ -200 +200 @@
-<Deployment/stream-api>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Deployment/stream-api>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -364 +364 @@
-<Deployment/stream-http-source>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Deployment/stream-http-source>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -525 +525 @@
-<Deployment/stream-storage-coordinator>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Deployment/stream-storage-coordinator>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -602 +602 @@
-<Endpoints/stream-etcd-headless>.subsets[0].addresses[0].hostname = "stream-etcd-1";
+<Endpoints/stream-etcd-headless>.subsets[0].addresses[0].hostname = "stream-etcd-2";
@@ -606 +606 @@
-<Endpoints/stream-etcd-headless>.subsets[0].addresses[0].targetRef.name = "stream-etcd-1";
+<Endpoints/stream-etcd-headless>.subsets[0].addresses[0].targetRef.name = "stream-etcd-2";
@@ -609 +609 @@
-<Endpoints/stream-etcd-headless>.subsets[0].addresses[1].hostname = "stream-etcd-2";
+<Endpoints/stream-etcd-headless>.subsets[0].addresses[1].hostname = "stream-etcd-1";
@@ -613 +613 @@
-<Endpoints/stream-etcd-headless>.subsets[0].addresses[1].targetRef.name = "stream-etcd-2";
+<Endpoints/stream-etcd-headless>.subsets[0].addresses[1].targetRef.name = "stream-etcd-1";
@@ -653 +653 @@
-<Endpoints/stream-etcd>.subsets[0].addresses[0].targetRef.name = "stream-etcd-1";
+<Endpoints/stream-etcd>.subsets[0].addresses[0].targetRef.name = "stream-etcd-2";
@@ -659 +659 @@
-<Endpoints/stream-etcd>.subsets[0].addresses[1].targetRef.name = "stream-etcd-2";
+<Endpoints/stream-etcd>.subsets[0].addresses[1].targetRef.name = "stream-etcd-1";
@@ -717 +717 @@
-<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[0].hostname = "stream-storage-writer-reader-0";
+<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[0].hostname = "stream-storage-writer-reader-1";
@@ -721 +721 @@
-<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[0].targetRef.name = "stream-storage-writer-reader-0";
+<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[0].targetRef.name = "stream-storage-writer-reader-1";
@@ -724 +724 @@
-<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[1].hostname = "stream-storage-writer-reader-1";
+<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[1].hostname = "stream-storage-writer-reader-0";
@@ -728 +728 @@
-<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[1].targetRef.name = "stream-storage-writer-reader-1";
+<Endpoints/stream-storage-writer-reader>.subsets[0].addresses[1].targetRef.name = "stream-storage-writer-reader-0";
@@ -1214,2 +1214,2 @@
-<Pod/stream-api-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
-<Pod/stream-api-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-api-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
+<Pod/stream-api-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -1534 +1534 @@
-<Pod/stream-etcd-0>.spec.containers[0].env[21].value = "new";
+<Pod/stream-etcd-0>.spec.containers[0].env[21].value = "existing";
@@ -1780 +1780 @@
-<Pod/stream-etcd-1>.spec.containers[0].env[21].value = "new";
+<Pod/stream-etcd-1>.spec.containers[0].env[21].value = "existing";
@@ -2026 +2026 @@
-<Pod/stream-etcd-2>.spec.containers[0].env[21].value = "new";
+<Pod/stream-etcd-2>.spec.containers[0].env[21].value = "existing";
@@ -2350,2 +2350,2 @@
-<Pod/stream-http-source-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
-<Pod/stream-http-source-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-http-source-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
+<Pod/stream-http-source-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -2742,2 +2742,2 @@
-<Pod/stream-storage-coordinator-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
-<Pod/stream-storage-coordinator-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-storage-coordinator-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
+<Pod/stream-storage-coordinator-<hash>>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -2978 +2978 @@
-<Pod/stream-storage-writer-reader-0>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-storage-writer-reader-0>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3061 +3061 @@
-<Pod/stream-storage-writer-reader-0>.spec.containers[1].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-storage-writer-reader-0>.spec.containers[1].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3231 +3231 @@
-<Pod/stream-storage-writer-reader-1>.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-storage-writer-reader-1>.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3314 +3314 @@
-<Pod/stream-storage-writer-reader-1>.spec.containers[1].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<Pod/stream-storage-writer-reader-1>.spec.containers[1].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3552 +3552 @@
-<ReplicaSet/stream-api-<hash>>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<ReplicaSet/stream-api-<hash>>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3723 +3723 @@
-<ReplicaSet/stream-http-source-<hash>>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<ReplicaSet/stream-http-source-<hash>>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3891 +3891 @@
-<ReplicaSet/stream-storage-coordinator-<hash>>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<ReplicaSet/stream-storage-coordinator-<hash>>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -3932,0 +3933,12 @@
+<Secret/sh.helm.release.v1.stream-etcd.v2> = {};
+<Secret/sh.helm.release.v1.stream-etcd.v2>.apiVersion = "v1";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.data = {};
+<Secret/sh.helm.release.v1.stream-etcd.v2>.kind = "Secret";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata = {};
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata.labels = {};
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata.labels.name = "stream-etcd";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata.labels.owner = "helm";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata.labels.version = "2";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata.name = "sh.helm.release.v1.stream-etcd.v2";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.metadata.namespace = "stream";
+<Secret/sh.helm.release.v1.stream-etcd.v2>.type = "helm.sh/release.v1";
@@ -4228 +4240 @@
-<StatefulSet/stream-etcd>.spec.template.spec.containers[0].env[21].value = "new";
+<StatefulSet/stream-etcd>.spec.template.spec.containers[0].env[21].value = "existing";
@@ -4478 +4490 @@
-<StatefulSet/stream-storage-writer-reader>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<StatefulSet/stream-storage-writer-reader>.spec.template.spec.containers[0].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";
@@ -4558 +4570 @@
-<StatefulSet/stream-storage-writer-reader>.spec.template.spec.containers[1].image = "docker.io/keboola/stream-api:aff7efa-1741353694";
+<StatefulSet/stream-storage-writer-reader>.spec.template.spec.containers[1].image = "docker.io/keboola/stream-api:8b22ddd-1741354138";


(see artifacts in the Github Action for more information)
```
</details>

<!-- Sticky Pull Request Commentstream-kubernetes-state-diff -->
author_association: NONE