From fe9089b96145188d1707d7f28eda229a16ba8994 Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Tue, 14 Jul 2026 13:00:23 -0300 Subject: [PATCH 1/4] fix: clear error when triggering job on undeployed scheduled task --- CHANGELOG.md | 3 + scheduled_task/scope/tests/trigger.bats | 174 ++++++++++++++++++++++++ scheduled_task/scope/trigger | 31 ++++- 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 scheduled_task/scope/tests/trigger.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa174b9..85362f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +- Fix: triggering a scheduled task job on a scope that is not deployed now fails with a clear "deploy the scope first" message instead of an opaque error + ## [1.13.0] - 2026-07-10 - Add support to get AWS credentials via assume role - Add support to auto-create ALBs on scope create diff --git a/scheduled_task/scope/tests/trigger.bats b/scheduled_task/scope/tests/trigger.bats new file mode 100644 index 00000000..b7cd1d75 --- /dev/null +++ b/scheduled_task/scope/tests/trigger.bats @@ -0,0 +1,174 @@ +#!/usr/bin/env bats +# ============================================================================= +# Unit tests for scheduled_task scope/trigger - manually trigger the CronJob +# +# Contract: +# - A scope with no active deployment has no CronJob, so triggering must fail +# early with a clear "deploy first" message instead of an opaque kubectl +# error from `kubectl create job --from=cronjob/`. +# ============================================================================= + +setup() { + export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + source "$PROJECT_ROOT/testing/assertions.sh" + + export K8S_NAMESPACE="default-namespace" + + # Base CONTEXT: deployed scope with an active deployment + export CONTEXT='{ + "scope": { + "id": "scope-123", + "current_active_deployment": "deploy-456" + }, + "providers": { + "container-orchestration": { + "cluster": { + "namespace": "provider-namespace" + } + } + } + }' + + # Mock kubectl: cronjob exists, job creation succeeds + kubectl() { + case "$*" in + "get cronjob -n provider-namespace -l scope_id=scope-123 -o jsonpath={.items[0].metadata.name}") + echo "my-cronjob" + return 0 + ;; + "create job --from=cronjob/my-cronjob"*) + return 0 + ;; + *) + return 0 + ;; + esac + } + export -f kubectl + + # Deterministic timestamp for job name suffix + date() { echo "1700000000"; } + export -f date +} + +teardown() { + unset -f kubectl date +} + +# ============================================================================= +# Success Flow +# ============================================================================= +@test "trigger: success flow - finds cronjob and triggers a job" { + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 0 ] + assert_contains "$output" "Triggering job my-cronjob" + assert_contains "$output" "The job my-cronjob was triggered, you can follow the execution from the logs screen" +} + +# ============================================================================= +# Error: scope not deployed (no active deployment) +# ============================================================================= +@test "trigger: fails with a clear message when the scope has no active deployment" { + export CONTEXT=$(echo "$CONTEXT" | jq 'del(.scope.current_active_deployment)') + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 1 ] + assert_contains "$output" "❌ The scope has no active deployment, so there is no scheduled job to trigger" + assert_contains "$output" "💡 Possible causes:" + assert_contains "$output" "The scope has not been deployed yet" + assert_contains "$output" "🔧 How to fix:" + assert_contains "$output" "• Deploy the scope first, then trigger the job" +} + +@test "trigger: fails when current_active_deployment is null" { + export CONTEXT=$(echo "$CONTEXT" | jq '.scope.current_active_deployment = null') + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 1 ] + assert_contains "$output" "❌ The scope has no active deployment, so there is no scheduled job to trigger" + assert_contains "$output" "• Deploy the scope first, then trigger the job" +} + +# ============================================================================= +# Error: deployed but no CronJob found +# ============================================================================= +@test "trigger: fails with a clear message when no CronJob exists for the scope" { + kubectl() { + case "$*" in + "get cronjob -n provider-namespace -l scope_id=scope-123 -o jsonpath={.items[0].metadata.name}") + echo "" + return 0 + ;; + *) + return 0 + ;; + esac + } + export -f kubectl + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 1 ] + assert_contains "$output" "❌ No CronJob found for scope 'scope-123' in namespace 'provider-namespace'" + assert_contains "$output" "💡 Possible causes:" + assert_contains "$output" "🔧 How to fix:" +} + +# ============================================================================= +# Namespace resolution +# ============================================================================= +@test "trigger: uses the namespace from the provider for lookup and job creation" { + # kubectl only succeeds when called against the provider namespace; any other + # namespace makes it fail, so this asserts the resolved namespace is threaded + # through both the cronjob lookup and the job creation. + kubectl() { + case "$*" in + "get cronjob -n provider-namespace"*) + echo "my-cronjob" + return 0 + ;; + "create job --from=cronjob/my-cronjob"*"-n provider-namespace") + return 0 + ;; + *) + echo "unexpected namespace: $*" >&2 + return 1 + ;; + esac + } + export -f kubectl + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 0 ] + assert_contains "$output" "The job my-cronjob was triggered, you can follow the execution from the logs screen" +} + +@test "trigger: falls back to the default namespace when the provider namespace is not set" { + export CONTEXT=$(echo "$CONTEXT" | jq 'del(.providers["container-orchestration"].cluster.namespace)') + + kubectl() { + case "$*" in + "get cronjob -n default-namespace"*) + echo "my-cronjob" + return 0 + ;; + "create job --from=cronjob/my-cronjob"*"-n default-namespace") + return 0 + ;; + *) + echo "unexpected namespace: $*" >&2 + return 1 + ;; + esac + } + export -f kubectl + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 0 ] + assert_contains "$output" "The job my-cronjob was triggered, you can follow the execution from the logs screen" +} diff --git a/scheduled_task/scope/trigger b/scheduled_task/scope/trigger index 1ebf6cce..ae1f2928 100644 --- a/scheduled_task/scope/trigger +++ b/scheduled_task/scope/trigger @@ -1,5 +1,21 @@ #!/bin/bash +set -euo pipefail + +DEPLOYMENT_ID=$(echo "$CONTEXT" | jq -r '.scope.current_active_deployment // empty') + +if [[ -z "$DEPLOYMENT_ID" ]]; then + echo "❌ The scope has no active deployment, so there is no scheduled job to trigger" >&2 + echo "" >&2 + echo "💡 Possible causes:" >&2 + echo " The scope has not been deployed yet" >&2 + echo "" >&2 + echo "🔧 How to fix:" >&2 + echo " • Deploy the scope first, then trigger the job" >&2 + echo "" >&2 + exit 1 +fi + K8S_NAMESPACE=$(echo "$CONTEXT" | jq -r --arg default "$K8S_NAMESPACE" ' .providers["container-orchestration"].cluster.namespace // $default ') @@ -8,8 +24,21 @@ SCOPE_ID=$(echo "$CONTEXT" | jq -r '.scope.id') JOB=$(kubectl get cronjob -n "$K8S_NAMESPACE" -l "scope_id=$SCOPE_ID" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null || echo "") +if [[ -z "$JOB" ]]; then + echo "❌ No CronJob found for scope '$SCOPE_ID' in namespace '$K8S_NAMESPACE'" >&2 + echo "" >&2 + echo "💡 Possible causes:" >&2 + echo " The scope's scheduled job may not have been created yet, or the deployment is still in progress" >&2 + echo "" >&2 + echo "🔧 How to fix:" >&2 + echo " • Verify the CronJob exists: kubectl get cronjob -n $K8S_NAMESPACE -l scope_id=$SCOPE_ID" >&2 + echo " • Redeploy the scope if the CronJob is missing" >&2 + echo "" >&2 + exit 1 +fi + echo "Triggering job $JOB" kubectl create job --from="cronjob/$JOB" "$JOB-$(date +%s)" -n "$K8S_NAMESPACE" -echo "The job $JOB was triggered, you can follow the execution from the logs screen" \ No newline at end of file +echo "The job $JOB was triggered, you can follow the execution from the logs screen" From ce46206ce9b5786436c3b85ca9949cfa6562bb0d Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Tue, 14 Jul 2026 13:54:16 -0300 Subject: [PATCH 2/4] refactor: use log helper in scheduled task trigger via workflow step --- scheduled_task/logging | 41 +++++++++++++++++++ scheduled_task/scope/tests/trigger.bats | 15 ++++--- scheduled_task/scope/trigger | 38 ++++++++--------- .../scope/workflows/trigger-job.yaml | 11 ++++- 4 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 scheduled_task/logging diff --git a/scheduled_task/logging b/scheduled_task/logging new file mode 100644 index 00000000..d0df55d7 --- /dev/null +++ b/scheduled_task/logging @@ -0,0 +1,41 @@ +#!/bin/bash + +# Logging utility — log4j-style level filtering +# Usage: log "level" "message" +# Levels: debug < info < warn < error +# Control: LOG_LEVEL env var (default: info) +# +# Example: +# LOG_LEVEL=info +# log debug "verbose details" # suppressed +# log info "deployment done" # shown +log() { + local level="${1:-info}" + local message="${2:-}" + + local -i msg_num threshold + + case "${level,,}" in + debug) msg_num=0 ;; + info) msg_num=1 ;; + warn) msg_num=2 ;; + error) msg_num=3 ;; + *) msg_num=1 ;; + esac + + case "${LOG_LEVEL:-info}" in + debug) threshold=0 ;; + info) threshold=1 ;; + warn) threshold=2 ;; + error) threshold=3 ;; + *) threshold=1 ;; + esac + + if [ "$msg_num" -ge "$threshold" ]; then + if [ "$msg_num" -ge 3 ]; then + echo "$message" >&2 + else + echo "$message" + fi + fi +} diff --git a/scheduled_task/scope/tests/trigger.bats b/scheduled_task/scope/tests/trigger.bats index b7cd1d75..92e33fad 100644 --- a/scheduled_task/scope/tests/trigger.bats +++ b/scheduled_task/scope/tests/trigger.bats @@ -12,6 +12,11 @@ setup() { export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" source "$PROJECT_ROOT/testing/assertions.sh" + # The workflow loads the `log` function via a `load logging` step, so the + # script assumes it exists. Mock it here (errors go to stderr, like the real one). + log() { if [ "$1" = "error" ]; then echo "$2" >&2; else echo "$2"; fi; } + export -f log + export K8S_NAMESPACE="default-namespace" # Base CONTEXT: deployed scope with an active deployment @@ -52,7 +57,7 @@ setup() { } teardown() { - unset -f kubectl date + unset -f kubectl date log } # ============================================================================= @@ -62,8 +67,8 @@ teardown() { run bash "$BATS_TEST_DIRNAME/../trigger" [ "$status" -eq 0 ] - assert_contains "$output" "Triggering job my-cronjob" - assert_contains "$output" "The job my-cronjob was triggered, you can follow the execution from the logs screen" + assert_contains "$output" "📝 Triggering job my-cronjob" + assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" } # ============================================================================= @@ -144,7 +149,7 @@ teardown() { run bash "$BATS_TEST_DIRNAME/../trigger" [ "$status" -eq 0 ] - assert_contains "$output" "The job my-cronjob was triggered, you can follow the execution from the logs screen" + assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" } @test "trigger: falls back to the default namespace when the provider namespace is not set" { @@ -170,5 +175,5 @@ teardown() { run bash "$BATS_TEST_DIRNAME/../trigger" [ "$status" -eq 0 ] - assert_contains "$output" "The job my-cronjob was triggered, you can follow the execution from the logs screen" + assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" } diff --git a/scheduled_task/scope/trigger b/scheduled_task/scope/trigger index ae1f2928..7a2f47d7 100644 --- a/scheduled_task/scope/trigger +++ b/scheduled_task/scope/trigger @@ -5,14 +5,14 @@ set -euo pipefail DEPLOYMENT_ID=$(echo "$CONTEXT" | jq -r '.scope.current_active_deployment // empty') if [[ -z "$DEPLOYMENT_ID" ]]; then - echo "❌ The scope has no active deployment, so there is no scheduled job to trigger" >&2 - echo "" >&2 - echo "💡 Possible causes:" >&2 - echo " The scope has not been deployed yet" >&2 - echo "" >&2 - echo "🔧 How to fix:" >&2 - echo " • Deploy the scope first, then trigger the job" >&2 - echo "" >&2 + log error "❌ The scope has no active deployment, so there is no scheduled job to trigger" + log error "" + log error "💡 Possible causes:" + log error " The scope has not been deployed yet" + log error "" + log error "🔧 How to fix:" + log error " • Deploy the scope first, then trigger the job" + log error "" exit 1 fi @@ -25,20 +25,20 @@ SCOPE_ID=$(echo "$CONTEXT" | jq -r '.scope.id') JOB=$(kubectl get cronjob -n "$K8S_NAMESPACE" -l "scope_id=$SCOPE_ID" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null || echo "") if [[ -z "$JOB" ]]; then - echo "❌ No CronJob found for scope '$SCOPE_ID' in namespace '$K8S_NAMESPACE'" >&2 - echo "" >&2 - echo "💡 Possible causes:" >&2 - echo " The scope's scheduled job may not have been created yet, or the deployment is still in progress" >&2 - echo "" >&2 - echo "🔧 How to fix:" >&2 - echo " • Verify the CronJob exists: kubectl get cronjob -n $K8S_NAMESPACE -l scope_id=$SCOPE_ID" >&2 - echo " • Redeploy the scope if the CronJob is missing" >&2 - echo "" >&2 + log error "❌ No CronJob found for scope '$SCOPE_ID' in namespace '$K8S_NAMESPACE'" + log error "" + log error "💡 Possible causes:" + log error " The scope's scheduled job may not have been created yet, or the deployment is still in progress" + log error "" + log error "🔧 How to fix:" + log error " • Verify the CronJob exists: kubectl get cronjob -n $K8S_NAMESPACE -l scope_id=$SCOPE_ID" + log error " • Redeploy the scope if the CronJob is missing" + log error "" exit 1 fi -echo "Triggering job $JOB" +log info "📝 Triggering job $JOB" kubectl create job --from="cronjob/$JOB" "$JOB-$(date +%s)" -n "$K8S_NAMESPACE" -echo "The job $JOB was triggered, you can follow the execution from the logs screen" +log info "✅ The job $JOB was triggered, you can follow the execution from the logs screen" diff --git a/scheduled_task/scope/workflows/trigger-job.yaml b/scheduled_task/scope/workflows/trigger-job.yaml index 6b07c1ce..954bccbd 100644 --- a/scheduled_task/scope/workflows/trigger-job.yaml +++ b/scheduled_task/scope/workflows/trigger-job.yaml @@ -2,6 +2,15 @@ provider_categories: - container-orchestration - cloud-providers steps: + - name: load logging + type: script + file: "$OVERRIDES_PATH/logging" + output: + - name: log + type: function + parameters: + level: string + message: string - name: trigger type: script - file: "$OVERRIDES_PATH/scope/trigger" \ No newline at end of file + file: "$OVERRIDES_PATH/scope/trigger" From 2d38eb9378cc06e76736586dc11e6932ce8ce6ab Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Tue, 14 Jul 2026 14:56:05 -0300 Subject: [PATCH 3/4] fix: guard K8S_NAMESPACE default under set -u in scheduled task trigger --- scheduled_task/scope/tests/trigger.bats | 39 +++++++++++++++++++++++++ scheduled_task/scope/trigger | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/scheduled_task/scope/tests/trigger.bats b/scheduled_task/scope/tests/trigger.bats index 92e33fad..6ffd2576 100644 --- a/scheduled_task/scope/tests/trigger.bats +++ b/scheduled_task/scope/tests/trigger.bats @@ -177,3 +177,42 @@ teardown() { [ "$status" -eq 0 ] assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" } + +# The trigger-job workflow does not include values.yaml, so K8S_NAMESPACE is +# unset at runtime. Under `set -u` an unguarded expansion aborts the script; +# these reproduce that real environment. +@test "trigger: does not abort under set -u when K8S_NAMESPACE is unset (uses provider namespace)" { + unset K8S_NAMESPACE + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 0 ] + assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" +} + +@test "trigger: falls back to the nullplatform default when K8S_NAMESPACE is unset and the provider namespace is absent" { + unset K8S_NAMESPACE + export CONTEXT=$(echo "$CONTEXT" | jq 'del(.providers["container-orchestration"].cluster.namespace)') + + kubectl() { + case "$*" in + "get cronjob -n nullplatform"*) + echo "my-cronjob" + return 0 + ;; + "create job --from=cronjob/my-cronjob"*"-n nullplatform") + return 0 + ;; + *) + echo "unexpected namespace: $*" >&2 + return 1 + ;; + esac + } + export -f kubectl + + run bash "$BATS_TEST_DIRNAME/../trigger" + + [ "$status" -eq 0 ] + assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" +} diff --git a/scheduled_task/scope/trigger b/scheduled_task/scope/trigger index 7a2f47d7..0aeefc5a 100644 --- a/scheduled_task/scope/trigger +++ b/scheduled_task/scope/trigger @@ -16,7 +16,7 @@ if [[ -z "$DEPLOYMENT_ID" ]]; then exit 1 fi -K8S_NAMESPACE=$(echo "$CONTEXT" | jq -r --arg default "$K8S_NAMESPACE" ' +K8S_NAMESPACE=$(echo "$CONTEXT" | jq -r --arg default "${K8S_NAMESPACE:-nullplatform}" ' .providers["container-orchestration"].cluster.namespace // $default ') From ec5e621b8fa4a29c5ef8b5081837c15c307d344a Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Tue, 14 Jul 2026 15:38:32 -0300 Subject: [PATCH 4/4] fix: include values.yaml in trigger-job workflow to set K8S_NAMESPACE --- scheduled_task/scope/tests/trigger.bats | 24 ++++++++++++++++--- .../scope/workflows/trigger-job.yaml | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/scheduled_task/scope/tests/trigger.bats b/scheduled_task/scope/tests/trigger.bats index 6ffd2576..18ff2732 100644 --- a/scheduled_task/scope/tests/trigger.bats +++ b/scheduled_task/scope/tests/trigger.bats @@ -178,9 +178,9 @@ teardown() { assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" } -# The trigger-job workflow does not include values.yaml, so K8S_NAMESPACE is -# unset at runtime. Under `set -u` an unguarded expansion aborts the script; -# these reproduce that real environment. +# The workflow includes values.yaml to set K8S_NAMESPACE, but the script also +# guards the expansion so that a missing include can never abort it under +# `set -u`. These reproduce that unguarded environment (K8S_NAMESPACE unset). @test "trigger: does not abort under set -u when K8S_NAMESPACE is unset (uses provider namespace)" { unset K8S_NAMESPACE @@ -216,3 +216,21 @@ teardown() { [ "$status" -eq 0 ] assert_contains "$output" "✅ The job my-cronjob was triggered, you can follow the execution from the logs screen" } + +# ============================================================================= +# Workflow wiring (trigger-job.yaml) — the layer the script tests can't see +# ============================================================================= +@test "trigger-job workflow includes values.yaml so K8S_NAMESPACE is provided" { + run grep -A2 "^include:" "$BATS_TEST_DIRNAME/../workflows/trigger-job.yaml" + + assert_equal "$status" "0" + assert_contains "$output" "values.yaml" +} + +@test "trigger-job workflow loads the log function before the trigger step" { + run cat "$BATS_TEST_DIRNAME/../workflows/trigger-job.yaml" + + assert_equal "$status" "0" + assert_contains "$output" "name: load logging" + assert_contains "$output" "\$OVERRIDES_PATH/logging" +} diff --git a/scheduled_task/scope/workflows/trigger-job.yaml b/scheduled_task/scope/workflows/trigger-job.yaml index 954bccbd..02df28f0 100644 --- a/scheduled_task/scope/workflows/trigger-job.yaml +++ b/scheduled_task/scope/workflows/trigger-job.yaml @@ -1,3 +1,5 @@ +include: + - "$SERVICE_PATH/values.yaml" provider_categories: - container-orchestration - cloud-providers